Skip to main content

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