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