koprogo_api/application/use_cases/
etat_date_use_cases.rs

1use crate::application::dto::{
2    CreateEtatDateRequest, EtatDateResponse, EtatDateStatsResponse, PageRequest,
3    UpdateEtatDateAdditionalDataRequest, UpdateEtatDateFinancialRequest,
4};
5use crate::application::ports::{
6    BuildingRepository, EtatDateRepository, UnitOwnerRepository, UnitRepository,
7};
8use crate::domain::entities::{EtatDate, EtatDateStatus};
9use f64;
10use std::sync::Arc;
11use uuid::Uuid;
12
13pub struct EtatDateUseCases {
14    repository: Arc<dyn EtatDateRepository>,
15    unit_repository: Arc<dyn UnitRepository>,
16    building_repository: Arc<dyn BuildingRepository>,
17    unit_owner_repository: Arc<dyn UnitOwnerRepository>,
18}
19
20impl EtatDateUseCases {
21    pub fn new(
22        repository: Arc<dyn EtatDateRepository>,
23        unit_repository: Arc<dyn UnitRepository>,
24        building_repository: Arc<dyn BuildingRepository>,
25        unit_owner_repository: Arc<dyn UnitOwnerRepository>,
26    ) -> Self {
27        Self {
28            repository,
29            unit_repository,
30            building_repository,
31            unit_owner_repository,
32        }
33    }
34
35    /// Create a new état daté request
36    pub async fn create_etat_date(
37        &self,
38        request: CreateEtatDateRequest,
39    ) -> Result<EtatDateResponse, String> {
40        // Verify unit exists
41        let unit = self
42            .unit_repository
43            .find_by_id(request.unit_id)
44            .await?
45            .ok_or_else(|| "Unit not found".to_string())?;
46
47        // Verify building exists
48        let building = self
49            .building_repository
50            .find_by_id(request.building_id)
51            .await?
52            .ok_or_else(|| "Building not found".to_string())?;
53
54        // Get unit ownership info (quotes-parts)
55        let unit_owners = self
56            .unit_owner_repository
57            .find_current_owners_by_unit(request.unit_id)
58            .await?;
59
60        if unit_owners.is_empty() {
61            return Err("Unit has no active owners".to_string());
62        }
63
64        // Calculate total quote-parts (should be 100% or close)
65        let total_quota: f64 = unit_owners.iter().map(|uo| uo.ownership_percentage).sum();
66
67        // For simplicity, use total quota as both ordinary and extraordinary
68        // In a real system, these might be stored separately per unit
69        let ordinary_charges_quota = total_quota * 100.0; // Convert to ownership_percentage
70        let extraordinary_charges_quota = ordinary_charges_quota;
71
72        // Create état daté
73        let etat_date = EtatDate::new(
74            request.organization_id,
75            request.building_id,
76            request.unit_id,
77            request.reference_date,
78            request.language,
79            request.notary_name,
80            request.notary_email,
81            request.notary_phone,
82            building.name.clone(),
83            building.address.clone(),
84            unit.unit_number.clone(),
85            unit.floor.map(|f| f.to_string()),
86            Some(unit.surface_area),
87            ordinary_charges_quota,
88            extraordinary_charges_quota,
89        )?;
90
91        let created = self.repository.create(&etat_date).await?;
92        Ok(EtatDateResponse::from(created))
93    }
94
95    /// Get état daté by ID
96    pub async fn get_etat_date(&self, id: Uuid) -> Result<Option<EtatDateResponse>, String> {
97        let etat_date = self.repository.find_by_id(id).await?;
98        Ok(etat_date.map(EtatDateResponse::from))
99    }
100
101    /// Get état daté by reference number
102    pub async fn get_by_reference_number(
103        &self,
104        reference_number: &str,
105    ) -> Result<Option<EtatDateResponse>, String> {
106        let etat_date = self
107            .repository
108            .find_by_reference_number(reference_number)
109            .await?;
110        Ok(etat_date.map(EtatDateResponse::from))
111    }
112
113    /// List états datés for a unit
114    pub async fn list_by_unit(&self, unit_id: Uuid) -> Result<Vec<EtatDateResponse>, String> {
115        let etats = self.repository.find_by_unit(unit_id).await?;
116        Ok(etats.into_iter().map(EtatDateResponse::from).collect())
117    }
118
119    /// List états datés for a building
120    pub async fn list_by_building(
121        &self,
122        building_id: Uuid,
123    ) -> Result<Vec<EtatDateResponse>, String> {
124        let etats = self.repository.find_by_building(building_id).await?;
125        Ok(etats.into_iter().map(EtatDateResponse::from).collect())
126    }
127
128    /// List états datés paginated
129    pub async fn list_paginated(
130        &self,
131        page_request: &PageRequest,
132        organization_id: Option<Uuid>,
133        status: Option<EtatDateStatus>,
134    ) -> Result<(Vec<EtatDateResponse>, i64), String> {
135        let (etats, total) = self
136            .repository
137            .find_all_paginated(page_request, organization_id, status)
138            .await?;
139
140        let dtos = etats.into_iter().map(EtatDateResponse::from).collect();
141        Ok((dtos, total))
142    }
143
144    /// Mark état daté as in progress
145    pub async fn mark_in_progress(&self, id: Uuid) -> Result<EtatDateResponse, String> {
146        let mut etat_date = self
147            .repository
148            .find_by_id(id)
149            .await?
150            .ok_or_else(|| "État daté not found".to_string())?;
151
152        etat_date.mark_in_progress()?;
153
154        let updated = self.repository.update(&etat_date).await?;
155        Ok(EtatDateResponse::from(updated))
156    }
157
158    /// Mark état daté as generated (with PDF file path)
159    pub async fn mark_generated(
160        &self,
161        id: Uuid,
162        pdf_file_path: String,
163    ) -> Result<EtatDateResponse, String> {
164        let mut etat_date = self
165            .repository
166            .find_by_id(id)
167            .await?
168            .ok_or_else(|| "État daté not found".to_string())?;
169
170        etat_date.mark_generated(pdf_file_path)?;
171
172        let updated = self.repository.update(&etat_date).await?;
173        Ok(EtatDateResponse::from(updated))
174    }
175
176    /// Mark état daté as delivered to notary
177    pub async fn mark_delivered(&self, id: Uuid) -> Result<EtatDateResponse, String> {
178        let mut etat_date = self
179            .repository
180            .find_by_id(id)
181            .await?
182            .ok_or_else(|| "État daté not found".to_string())?;
183
184        etat_date.mark_delivered()?;
185
186        let updated = self.repository.update(&etat_date).await?;
187        Ok(EtatDateResponse::from(updated))
188    }
189
190    /// Update financial data
191    pub async fn update_financial_data(
192        &self,
193        id: Uuid,
194        request: UpdateEtatDateFinancialRequest,
195    ) -> Result<EtatDateResponse, String> {
196        let mut etat_date = self
197            .repository
198            .find_by_id(id)
199            .await?
200            .ok_or_else(|| "État daté not found".to_string())?;
201
202        etat_date.update_financial_data(
203            request.owner_balance,
204            request.arrears_amount,
205            request.monthly_provision_amount,
206            request.total_balance,
207            request.approved_works_unpaid,
208        )?;
209
210        let updated = self.repository.update(&etat_date).await?;
211        Ok(EtatDateResponse::from(updated))
212    }
213
214    /// Update additional data (sections 7-16)
215    pub async fn update_additional_data(
216        &self,
217        id: Uuid,
218        request: UpdateEtatDateAdditionalDataRequest,
219    ) -> Result<EtatDateResponse, String> {
220        let mut etat_date = self
221            .repository
222            .find_by_id(id)
223            .await?
224            .ok_or_else(|| "État daté not found".to_string())?;
225
226        etat_date.update_additional_data(request.additional_data)?;
227
228        let updated = self.repository.update(&etat_date).await?;
229        Ok(EtatDateResponse::from(updated))
230    }
231
232    /// Get overdue états datés (>10 days, not generated yet)
233    pub async fn list_overdue(
234        &self,
235        organization_id: Uuid,
236    ) -> Result<Vec<EtatDateResponse>, String> {
237        let etats = self.repository.find_overdue(organization_id).await?;
238        Ok(etats.into_iter().map(EtatDateResponse::from).collect())
239    }
240
241    /// Get expired états datés (>3 months from reference date)
242    pub async fn list_expired(
243        &self,
244        organization_id: Uuid,
245    ) -> Result<Vec<EtatDateResponse>, String> {
246        let etats = self.repository.find_expired(organization_id).await?;
247        Ok(etats.into_iter().map(EtatDateResponse::from).collect())
248    }
249
250    /// Delete état daté
251    pub async fn delete_etat_date(&self, id: Uuid) -> Result<bool, String> {
252        self.repository.delete(id).await
253    }
254
255    /// Get statistics for dashboard
256    pub async fn get_stats(&self, organization_id: Uuid) -> Result<EtatDateStatsResponse, String> {
257        self.repository.get_stats(organization_id).await
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264    use crate::application::dto::EtatDateStatsResponse;
265    use crate::application::ports::{
266        BuildingRepository, EtatDateRepository, UnitOwnerRepository, UnitRepository,
267    };
268    use crate::domain::entities::{
269        Building, EtatDate, EtatDateLanguage, EtatDateStatus, Unit, UnitOwner, UnitType,
270    };
271    use chrono::Utc;
272    use mockall::mock;
273    use mockall::predicate::*;
274
275    // --- Mock repositories ---
276
277    mock! {
278        pub EtatDateRepo {}
279
280        #[async_trait::async_trait]
281        impl EtatDateRepository for EtatDateRepo {
282            async fn create(&self, etat_date: &EtatDate) -> Result<EtatDate, String>;
283            async fn find_by_id(&self, id: Uuid) -> Result<Option<EtatDate>, String>;
284            async fn find_by_reference_number(&self, reference_number: &str) -> Result<Option<EtatDate>, String>;
285            async fn find_by_unit(&self, unit_id: Uuid) -> Result<Vec<EtatDate>, String>;
286            async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<EtatDate>, String>;
287            async fn find_all_paginated(
288                &self,
289                page_request: &PageRequest,
290                organization_id: Option<Uuid>,
291                status: Option<EtatDateStatus>,
292            ) -> Result<(Vec<EtatDate>, i64), String>;
293            async fn find_overdue(&self, organization_id: Uuid) -> Result<Vec<EtatDate>, String>;
294            async fn find_expired(&self, organization_id: Uuid) -> Result<Vec<EtatDate>, String>;
295            async fn update(&self, etat_date: &EtatDate) -> Result<EtatDate, String>;
296            async fn delete(&self, id: Uuid) -> Result<bool, String>;
297            async fn get_stats(&self, organization_id: Uuid) -> Result<EtatDateStatsResponse, String>;
298            async fn count_by_status(&self, organization_id: Uuid, status: EtatDateStatus) -> Result<i64, String>;
299        }
300    }
301
302    mock! {
303        pub UnitRepo {}
304
305        #[async_trait::async_trait]
306        impl UnitRepository for UnitRepo {
307            async fn create(&self, unit: &Unit) -> Result<Unit, String>;
308            async fn find_by_id(&self, id: Uuid) -> Result<Option<Unit>, String>;
309            async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<Unit>, String>;
310            async fn find_by_owner(&self, owner_id: Uuid) -> Result<Vec<Unit>, String>;
311            async fn find_all_paginated(
312                &self,
313                page_request: &crate::application::dto::PageRequest,
314                filters: &crate::application::dto::UnitFilters,
315            ) -> Result<(Vec<Unit>, i64), String>;
316            async fn update(&self, unit: &Unit) -> Result<Unit, String>;
317            async fn delete(&self, id: Uuid) -> Result<bool, String>;
318        }
319    }
320
321    mock! {
322        pub BuildingRepo {}
323
324        #[async_trait::async_trait]
325        impl BuildingRepository for BuildingRepo {
326            async fn create(&self, building: &Building) -> Result<Building, String>;
327            async fn find_by_id(&self, id: Uuid) -> Result<Option<Building>, String>;
328            async fn find_by_slug(&self, slug: &str) -> Result<Option<Building>, String>;
329            async fn find_all(&self) -> Result<Vec<Building>, String>;
330            async fn find_all_paginated(
331                &self,
332                page_request: &crate::application::dto::PageRequest,
333                filters: &crate::application::dto::BuildingFilters,
334            ) -> Result<(Vec<Building>, i64), String>;
335            async fn update(&self, building: &Building) -> Result<Building, String>;
336            async fn delete(&self, id: Uuid) -> Result<bool, String>;
337        }
338    }
339
340    mock! {
341        pub UnitOwnerRepo {}
342
343        #[async_trait::async_trait]
344        impl UnitOwnerRepository for UnitOwnerRepo {
345            async fn create(&self, unit_owner: &UnitOwner) -> Result<UnitOwner, String>;
346            async fn find_by_id(&self, id: Uuid) -> Result<Option<UnitOwner>, String>;
347            async fn find_current_owners_by_unit(&self, unit_id: Uuid) -> Result<Vec<UnitOwner>, String>;
348            async fn find_current_units_by_owner(&self, owner_id: Uuid) -> Result<Vec<UnitOwner>, String>;
349            async fn find_all_owners_by_unit(&self, unit_id: Uuid) -> Result<Vec<UnitOwner>, String>;
350            async fn find_all_units_by_owner(&self, owner_id: Uuid) -> Result<Vec<UnitOwner>, String>;
351            async fn update(&self, unit_owner: &UnitOwner) -> Result<UnitOwner, String>;
352            async fn delete(&self, id: Uuid) -> Result<(), String>;
353            async fn has_active_owners(&self, unit_id: Uuid) -> Result<bool, String>;
354            async fn get_total_ownership_percentage(&self, unit_id: Uuid) -> Result<f64, String>;
355            async fn find_active_by_unit_and_owner(&self, unit_id: Uuid, owner_id: Uuid) -> Result<Option<UnitOwner>, String>;
356            async fn find_active_by_building(&self, building_id: Uuid) -> Result<Vec<(Uuid, Uuid, f64)>, String>;
357        }
358    }
359
360    // --- Helpers ---
361
362    fn make_building(org_id: Uuid) -> Building {
363        Building::new(
364            org_id,
365            "Résidence Les Jardins".to_string(),
366            "Rue de la Loi 123".to_string(),
367            "Bruxelles".to_string(),
368            "1000".to_string(),
369            "Belgium".to_string(),
370            25,
371            1000,
372            Some(2020),
373        )
374        .unwrap()
375    }
376
377    fn make_unit(org_id: Uuid, building_id: Uuid) -> Unit {
378        Unit::new(
379            org_id,
380            building_id,
381            "101".to_string(),
382            UnitType::Apartment,
383            Some(1),
384            85.0,
385            50.0,
386        )
387        .unwrap()
388    }
389
390    fn make_unit_owner(unit_id: Uuid) -> UnitOwner {
391        UnitOwner::new(unit_id, Uuid::new_v4(), 0.5, true).unwrap()
392    }
393
394    fn make_etat_date(org_id: Uuid, building_id: Uuid, unit_id: Uuid) -> EtatDate {
395        EtatDate::new(
396            org_id,
397            building_id,
398            unit_id,
399            Utc::now(),
400            EtatDateLanguage::Fr,
401            "Maitre Dupont".to_string(),
402            "dupont@notaire.be".to_string(),
403            Some("+32 2 123 4567".to_string()),
404            "Residence Les Jardins".to_string(),
405            "Rue de la Loi 123, 1000 Bruxelles".to_string(),
406            "101".to_string(),
407            Some("1".to_string()),
408            Some(85.0),
409            50.0,
410            50.0,
411        )
412        .unwrap()
413    }
414
415    fn make_create_request(
416        org_id: Uuid,
417        building_id: Uuid,
418        unit_id: Uuid,
419    ) -> CreateEtatDateRequest {
420        CreateEtatDateRequest {
421            organization_id: org_id,
422            building_id,
423            unit_id,
424            reference_date: Utc::now(),
425            language: EtatDateLanguage::Fr,
426            notary_name: "Maitre Dupont".to_string(),
427            notary_email: "dupont@notaire.be".to_string(),
428            notary_phone: Some("+32 2 123 4567".to_string()),
429        }
430    }
431
432    fn build_use_cases(
433        etat_repo: MockEtatDateRepo,
434        unit_repo: MockUnitRepo,
435        building_repo: MockBuildingRepo,
436        uo_repo: MockUnitOwnerRepo,
437    ) -> EtatDateUseCases {
438        EtatDateUseCases::new(
439            Arc::new(etat_repo),
440            Arc::new(unit_repo),
441            Arc::new(building_repo),
442            Arc::new(uo_repo),
443        )
444    }
445
446    // --- Tests ---
447
448    #[tokio::test]
449    async fn test_create_etat_date_success() {
450        let org_id = Uuid::new_v4();
451        let building_id = Uuid::new_v4();
452        let unit_id = Uuid::new_v4();
453
454        let mut etat_repo = MockEtatDateRepo::new();
455        let mut unit_repo = MockUnitRepo::new();
456        let mut building_repo = MockBuildingRepo::new();
457        let mut uo_repo = MockUnitOwnerRepo::new();
458
459        // Mock unit exists
460        let unit = make_unit(org_id, building_id);
461        unit_repo
462            .expect_find_by_id()
463            .with(eq(unit_id))
464            .times(1)
465            .returning(move |_| Ok(Some(unit.clone())));
466
467        // Mock building exists
468        let building = make_building(org_id);
469        building_repo
470            .expect_find_by_id()
471            .with(eq(building_id))
472            .times(1)
473            .returning(move |_| Ok(Some(building.clone())));
474
475        // Mock unit owners exist with 50% ownership
476        let uo = make_unit_owner(unit_id);
477        uo_repo
478            .expect_find_current_owners_by_unit()
479            .with(eq(unit_id))
480            .times(1)
481            .returning(move |_| Ok(vec![uo.clone()]));
482
483        // Mock create returns the entity
484        etat_repo
485            .expect_create()
486            .times(1)
487            .returning(|ed| Ok(ed.clone()));
488
489        let use_cases = build_use_cases(etat_repo, unit_repo, building_repo, uo_repo);
490        let request = make_create_request(org_id, building_id, unit_id);
491
492        let result = use_cases.create_etat_date(request).await;
493        assert!(result.is_ok());
494        let response = result.unwrap();
495        assert_eq!(response.status, EtatDateStatus::Requested);
496        assert!(response.reference_number.starts_with("ED-"));
497        assert_eq!(response.notary_name, "Maitre Dupont");
498        assert_eq!(response.notary_email, "dupont@notaire.be");
499    }
500
501    #[tokio::test]
502    async fn test_create_etat_date_unit_not_found() {
503        let org_id = Uuid::new_v4();
504        let building_id = Uuid::new_v4();
505        let unit_id = Uuid::new_v4();
506
507        let etat_repo = MockEtatDateRepo::new();
508        let mut unit_repo = MockUnitRepo::new();
509        let building_repo = MockBuildingRepo::new();
510        let uo_repo = MockUnitOwnerRepo::new();
511
512        // Mock unit not found
513        unit_repo
514            .expect_find_by_id()
515            .with(eq(unit_id))
516            .times(1)
517            .returning(|_| Ok(None));
518
519        let use_cases = build_use_cases(etat_repo, unit_repo, building_repo, uo_repo);
520        let request = make_create_request(org_id, building_id, unit_id);
521
522        let result = use_cases.create_etat_date(request).await;
523        assert!(result.is_err());
524        assert_eq!(result.unwrap_err(), "Unit not found");
525    }
526
527    #[tokio::test]
528    async fn test_create_etat_date_no_active_owners() {
529        let org_id = Uuid::new_v4();
530        let building_id = Uuid::new_v4();
531        let unit_id = Uuid::new_v4();
532
533        let etat_repo = MockEtatDateRepo::new();
534        let mut unit_repo = MockUnitRepo::new();
535        let mut building_repo = MockBuildingRepo::new();
536        let mut uo_repo = MockUnitOwnerRepo::new();
537
538        let unit = make_unit(org_id, building_id);
539        unit_repo
540            .expect_find_by_id()
541            .with(eq(unit_id))
542            .times(1)
543            .returning(move |_| Ok(Some(unit.clone())));
544
545        let building = make_building(org_id);
546        building_repo
547            .expect_find_by_id()
548            .with(eq(building_id))
549            .times(1)
550            .returning(move |_| Ok(Some(building.clone())));
551
552        // No active owners
553        uo_repo
554            .expect_find_current_owners_by_unit()
555            .with(eq(unit_id))
556            .times(1)
557            .returning(|_| Ok(vec![]));
558
559        let use_cases = build_use_cases(etat_repo, unit_repo, building_repo, uo_repo);
560        let request = make_create_request(org_id, building_id, unit_id);
561
562        let result = use_cases.create_etat_date(request).await;
563        assert!(result.is_err());
564        assert_eq!(result.unwrap_err(), "Unit has no active owners");
565    }
566
567    #[tokio::test]
568    async fn test_find_by_id_success() {
569        let org_id = Uuid::new_v4();
570        let building_id = Uuid::new_v4();
571        let unit_id = Uuid::new_v4();
572        let etat_date = make_etat_date(org_id, building_id, unit_id);
573        let etat_id = etat_date.id;
574
575        let mut etat_repo = MockEtatDateRepo::new();
576        etat_repo
577            .expect_find_by_id()
578            .with(eq(etat_id))
579            .times(1)
580            .returning(move |_| Ok(Some(etat_date.clone())));
581
582        let use_cases = build_use_cases(
583            etat_repo,
584            MockUnitRepo::new(),
585            MockBuildingRepo::new(),
586            MockUnitOwnerRepo::new(),
587        );
588
589        let result = use_cases.get_etat_date(etat_id).await;
590        assert!(result.is_ok());
591        let response = result.unwrap();
592        assert!(response.is_some());
593        assert_eq!(response.unwrap().id, etat_id);
594    }
595
596    #[tokio::test]
597    async fn test_find_by_reference_number_success() {
598        let org_id = Uuid::new_v4();
599        let building_id = Uuid::new_v4();
600        let unit_id = Uuid::new_v4();
601        let etat_date = make_etat_date(org_id, building_id, unit_id);
602        let ref_number = etat_date.reference_number.clone();
603
604        let mut etat_repo = MockEtatDateRepo::new();
605        etat_repo
606            .expect_find_by_reference_number()
607            .with(eq(ref_number.clone()))
608            .times(1)
609            .returning(move |_| Ok(Some(etat_date.clone())));
610
611        let use_cases = build_use_cases(
612            etat_repo,
613            MockUnitRepo::new(),
614            MockBuildingRepo::new(),
615            MockUnitOwnerRepo::new(),
616        );
617
618        let result = use_cases.get_by_reference_number(&ref_number).await;
619        assert!(result.is_ok());
620        let response = result.unwrap();
621        assert!(response.is_some());
622        assert_eq!(response.unwrap().reference_number, ref_number);
623    }
624
625    #[tokio::test]
626    async fn test_mark_in_progress_success() {
627        let org_id = Uuid::new_v4();
628        let building_id = Uuid::new_v4();
629        let unit_id = Uuid::new_v4();
630        let etat_date = make_etat_date(org_id, building_id, unit_id);
631        let etat_id = etat_date.id;
632
633        let mut etat_repo = MockEtatDateRepo::new();
634
635        // find_by_id returns a Requested etat date
636        etat_repo
637            .expect_find_by_id()
638            .with(eq(etat_id))
639            .times(1)
640            .returning(move |_| Ok(Some(etat_date.clone())));
641
642        // update returns the updated entity
643        etat_repo
644            .expect_update()
645            .times(1)
646            .returning(|ed| Ok(ed.clone()));
647
648        let use_cases = build_use_cases(
649            etat_repo,
650            MockUnitRepo::new(),
651            MockBuildingRepo::new(),
652            MockUnitOwnerRepo::new(),
653        );
654
655        let result = use_cases.mark_in_progress(etat_id).await;
656        assert!(result.is_ok());
657        let response = result.unwrap();
658        assert_eq!(response.status, EtatDateStatus::InProgress);
659    }
660
661    #[tokio::test]
662    async fn test_mark_generated_with_pdf_path() {
663        let org_id = Uuid::new_v4();
664        let building_id = Uuid::new_v4();
665        let unit_id = Uuid::new_v4();
666        let mut etat_date = make_etat_date(org_id, building_id, unit_id);
667        // Must be InProgress to transition to Generated
668        etat_date.status = EtatDateStatus::InProgress;
669        let etat_id = etat_date.id;
670
671        let mut etat_repo = MockEtatDateRepo::new();
672
673        etat_repo
674            .expect_find_by_id()
675            .with(eq(etat_id))
676            .times(1)
677            .returning(move |_| Ok(Some(etat_date.clone())));
678
679        etat_repo
680            .expect_update()
681            .times(1)
682            .returning(|ed| Ok(ed.clone()));
683
684        let use_cases = build_use_cases(
685            etat_repo,
686            MockUnitRepo::new(),
687            MockBuildingRepo::new(),
688            MockUnitOwnerRepo::new(),
689        );
690
691        let pdf_path = "/documents/etat-date/ED-2026-001.pdf".to_string();
692        let result = use_cases.mark_generated(etat_id, pdf_path.clone()).await;
693        assert!(result.is_ok());
694        let response = result.unwrap();
695        assert_eq!(response.status, EtatDateStatus::Generated);
696        assert_eq!(response.pdf_file_path, Some(pdf_path));
697        assert!(response.generated_date.is_some());
698    }
699
700    #[tokio::test]
701    async fn test_mark_delivered_to_notary() {
702        let org_id = Uuid::new_v4();
703        let building_id = Uuid::new_v4();
704        let unit_id = Uuid::new_v4();
705        let mut etat_date = make_etat_date(org_id, building_id, unit_id);
706        // Must be Generated to transition to Delivered
707        etat_date.status = EtatDateStatus::Generated;
708        etat_date.generated_date = Some(Utc::now());
709        etat_date.pdf_file_path = Some("/documents/etat-date/ED-2026-001.pdf".to_string());
710        let etat_id = etat_date.id;
711
712        let mut etat_repo = MockEtatDateRepo::new();
713
714        etat_repo
715            .expect_find_by_id()
716            .with(eq(etat_id))
717            .times(1)
718            .returning(move |_| Ok(Some(etat_date.clone())));
719
720        etat_repo
721            .expect_update()
722            .times(1)
723            .returning(|ed| Ok(ed.clone()));
724
725        let use_cases = build_use_cases(
726            etat_repo,
727            MockUnitRepo::new(),
728            MockBuildingRepo::new(),
729            MockUnitOwnerRepo::new(),
730        );
731
732        let result = use_cases.mark_delivered(etat_id).await;
733        assert!(result.is_ok());
734        let response = result.unwrap();
735        assert_eq!(response.status, EtatDateStatus::Delivered);
736        assert!(response.delivered_date.is_some());
737    }
738
739    #[tokio::test]
740    async fn test_update_financial_data_success() {
741        let org_id = Uuid::new_v4();
742        let building_id = Uuid::new_v4();
743        let unit_id = Uuid::new_v4();
744        let etat_date = make_etat_date(org_id, building_id, unit_id);
745        let etat_id = etat_date.id;
746
747        let mut etat_repo = MockEtatDateRepo::new();
748
749        etat_repo
750            .expect_find_by_id()
751            .with(eq(etat_id))
752            .times(1)
753            .returning(move |_| Ok(Some(etat_date.clone())));
754
755        etat_repo
756            .expect_update()
757            .times(1)
758            .returning(|ed| Ok(ed.clone()));
759
760        let use_cases = build_use_cases(
761            etat_repo,
762            MockUnitRepo::new(),
763            MockBuildingRepo::new(),
764            MockUnitOwnerRepo::new(),
765        );
766
767        let request = UpdateEtatDateFinancialRequest {
768            owner_balance: -1250.50,
769            arrears_amount: 800.0,
770            monthly_provision_amount: 150.0,
771            total_balance: -1250.50,
772            approved_works_unpaid: 3500.0,
773        };
774
775        let result = use_cases.update_financial_data(etat_id, request).await;
776        assert!(result.is_ok());
777        let response = result.unwrap();
778        assert_eq!(response.owner_balance, -1250.50);
779        assert_eq!(response.arrears_amount, 800.0);
780        assert_eq!(response.monthly_provision_amount, 150.0);
781        assert_eq!(response.total_balance, -1250.50);
782        assert_eq!(response.approved_works_unpaid, 3500.0);
783    }
784
785    #[tokio::test]
786    async fn test_list_overdue_returns_old_requests() {
787        let org_id = Uuid::new_v4();
788        let building_id = Uuid::new_v4();
789        let unit_id = Uuid::new_v4();
790
791        let mut overdue_etat = make_etat_date(org_id, building_id, unit_id);
792        // Simulate a request made 16 days ago (>15 days = overdue per Art. 3.94 CC)
793        overdue_etat.requested_date = Utc::now() - chrono::Duration::days(16);
794
795        let mut etat_repo = MockEtatDateRepo::new();
796        etat_repo
797            .expect_find_overdue()
798            .with(eq(org_id))
799            .times(1)
800            .returning(move |_| Ok(vec![overdue_etat.clone()]));
801
802        let use_cases = build_use_cases(
803            etat_repo,
804            MockUnitRepo::new(),
805            MockBuildingRepo::new(),
806            MockUnitOwnerRepo::new(),
807        );
808
809        let result = use_cases.list_overdue(org_id).await;
810        assert!(result.is_ok());
811        let items = result.unwrap();
812        assert_eq!(items.len(), 1);
813        assert!(items[0].is_overdue);
814        assert!(items[0].days_since_request >= 16);
815    }
816
817    #[tokio::test]
818    async fn test_list_expired_returns_old_etats() {
819        let org_id = Uuid::new_v4();
820        let building_id = Uuid::new_v4();
821        let unit_id = Uuid::new_v4();
822
823        let mut expired_etat = make_etat_date(org_id, building_id, unit_id);
824        // Simulate reference date >3 months ago (>90 days = expired)
825        expired_etat.reference_date = Utc::now() - chrono::Duration::days(100);
826
827        let mut etat_repo = MockEtatDateRepo::new();
828        etat_repo
829            .expect_find_expired()
830            .with(eq(org_id))
831            .times(1)
832            .returning(move |_| Ok(vec![expired_etat.clone()]));
833
834        let use_cases = build_use_cases(
835            etat_repo,
836            MockUnitRepo::new(),
837            MockBuildingRepo::new(),
838            MockUnitOwnerRepo::new(),
839        );
840
841        let result = use_cases.list_expired(org_id).await;
842        assert!(result.is_ok());
843        let items = result.unwrap();
844        assert_eq!(items.len(), 1);
845        assert!(items[0].is_expired);
846    }
847
848    #[tokio::test]
849    async fn test_mark_in_progress_not_found() {
850        let etat_id = Uuid::new_v4();
851
852        let mut etat_repo = MockEtatDateRepo::new();
853        etat_repo
854            .expect_find_by_id()
855            .with(eq(etat_id))
856            .times(1)
857            .returning(|_| Ok(None));
858
859        let use_cases = build_use_cases(
860            etat_repo,
861            MockUnitRepo::new(),
862            MockBuildingRepo::new(),
863            MockUnitOwnerRepo::new(),
864        );
865
866        let result = use_cases.mark_in_progress(etat_id).await;
867        assert!(result.is_err());
868        assert!(result.unwrap_err().contains("not found"));
869    }
870}