koprogo_api/application/use_cases/
building_use_cases.rs

1use crate::application::dto::{
2    BuildingFilters, BuildingResponseDto, CreateBuildingDto, PageRequest, UpdateBuildingDto,
3};
4use crate::application::ports::BuildingRepository;
5use crate::domain::entities::Building;
6use std::sync::Arc;
7use uuid::Uuid;
8
9pub struct BuildingUseCases {
10    repository: Arc<dyn BuildingRepository>,
11}
12
13impl BuildingUseCases {
14    pub fn new(repository: Arc<dyn BuildingRepository>) -> Self {
15        Self { repository }
16    }
17
18    pub async fn create_building(
19        &self,
20        dto: CreateBuildingDto,
21    ) -> Result<BuildingResponseDto, String> {
22        let organization_id = Uuid::parse_str(&dto.organization_id)
23            .map_err(|_| "Invalid organization_id format".to_string())?;
24
25        let building = Building::new(
26            organization_id,
27            dto.name,
28            dto.address,
29            dto.city,
30            dto.postal_code,
31            dto.country,
32            dto.total_units,
33            dto.total_tantiemes.unwrap_or(1000),
34            dto.construction_year,
35        )?;
36
37        let created = self.repository.create(&building).await?;
38        Ok(self.to_response_dto(&created))
39    }
40
41    pub async fn get_building(&self, id: Uuid) -> Result<Option<BuildingResponseDto>, String> {
42        let building = self.repository.find_by_id(id).await?;
43        Ok(building.map(|b| self.to_response_dto(&b)))
44    }
45
46    pub async fn list_buildings(&self) -> Result<Vec<BuildingResponseDto>, String> {
47        let buildings = self.repository.find_all().await?;
48        Ok(buildings.iter().map(|b| self.to_response_dto(b)).collect())
49    }
50
51    pub async fn list_buildings_paginated(
52        &self,
53        page_request: &PageRequest,
54        organization_id: Option<Uuid>,
55    ) -> Result<(Vec<BuildingResponseDto>, i64), String> {
56        let filters = BuildingFilters {
57            organization_id,
58            ..Default::default()
59        };
60
61        let (buildings, total) = self
62            .repository
63            .find_all_paginated(page_request, &filters)
64            .await?;
65
66        let dtos = buildings.iter().map(|b| self.to_response_dto(b)).collect();
67        Ok((dtos, total))
68    }
69
70    /// Liste paginée avec filtrage Owner (BUG-WF14-2)
71    /// Si owner_user_id est Some, filtre les buildings où le user possède un lot
72    pub async fn list_buildings_paginated_for_user(
73        &self,
74        page_request: &PageRequest,
75        organization_id: Option<Uuid>,
76        owner_user_id: Option<Uuid>,
77    ) -> Result<(Vec<BuildingResponseDto>, i64), String> {
78        let filters = BuildingFilters {
79            organization_id,
80            owner_user_id,
81            ..Default::default()
82        };
83
84        let (buildings, total) = self
85            .repository
86            .find_all_paginated(page_request, &filters)
87            .await?;
88
89        let dtos = buildings.iter().map(|b| self.to_response_dto(b)).collect();
90        Ok((dtos, total))
91    }
92
93    pub async fn update_building(
94        &self,
95        id: Uuid,
96        dto: UpdateBuildingDto,
97    ) -> Result<BuildingResponseDto, String> {
98        let mut building = self
99            .repository
100            .find_by_id(id)
101            .await?
102            .ok_or_else(|| "Building not found".to_string())?;
103
104        // Update organization if provided (SuperAdmin feature)
105        if let Some(org_id_str) = dto.organization_id {
106            let org_id = Uuid::parse_str(&org_id_str)
107                .map_err(|_| "Invalid organization_id format".to_string())?;
108            building.organization_id = org_id;
109        }
110
111        building.update_info(
112            dto.name,
113            dto.address,
114            dto.city,
115            dto.postal_code,
116            dto.country,
117            dto.total_units,
118            dto.total_tantiemes.unwrap_or(1000),
119            dto.construction_year,
120        );
121
122        let updated = self.repository.update(&building).await?;
123        Ok(self.to_response_dto(&updated))
124    }
125
126    pub async fn delete_building(&self, id: Uuid) -> Result<bool, String> {
127        self.repository.delete(id).await
128    }
129
130    /// Find building by URL slug (for public pages - Issue #92)
131    pub async fn find_by_slug(&self, slug: &str) -> Result<Option<Building>, String> {
132        self.repository.find_by_slug(slug).await
133    }
134
135    fn to_response_dto(&self, building: &Building) -> BuildingResponseDto {
136        BuildingResponseDto {
137            id: building.id.to_string(),
138            organization_id: building.organization_id.to_string(),
139            name: building.name.clone(),
140            address: building.address.clone(),
141            city: building.city.clone(),
142            postal_code: building.postal_code.clone(),
143            country: building.country.clone(),
144            total_units: building.total_units,
145            total_tantiemes: building.total_tantiemes,
146            construction_year: building.construction_year,
147            created_at: building.created_at.to_rfc3339(),
148            updated_at: building.updated_at.to_rfc3339(),
149        }
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156    use crate::application::ports::BuildingRepository;
157    use async_trait::async_trait;
158    use mockall::mock;
159
160    mock! {
161        BuildingRepo {}
162
163        #[async_trait]
164        impl BuildingRepository for BuildingRepo {
165            async fn create(&self, building: &Building) -> Result<Building, String>;
166            async fn find_by_id(&self, id: Uuid) -> Result<Option<Building>, String>;
167            async fn find_all(&self) -> Result<Vec<Building>, String>;
168            async fn find_all_paginated(
169                &self,
170                page_request: &PageRequest,
171                filters: &BuildingFilters,
172            ) -> Result<(Vec<Building>, i64), String>;
173            async fn update(&self, building: &Building) -> Result<Building, String>;
174            async fn delete(&self, id: Uuid) -> Result<bool, String>;
175            async fn find_by_slug(&self, slug: &str) -> Result<Option<Building>, String>;
176        }
177    }
178
179    #[tokio::test]
180    async fn test_create_building_success() {
181        let mut mock_repo = MockBuildingRepo::new();
182
183        mock_repo.expect_create().returning(|b| Ok(b.clone()));
184
185        let use_cases = BuildingUseCases::new(Arc::new(mock_repo));
186
187        let dto = CreateBuildingDto {
188            organization_id: Uuid::new_v4().to_string(),
189            name: "Test Building".to_string(),
190            address: "123 Test St".to_string(),
191            city: "Paris".to_string(),
192            postal_code: "75001".to_string(),
193            country: "France".to_string(),
194            total_units: 10,
195            total_tantiemes: Some(1000),
196            construction_year: Some(2000),
197        };
198
199        let result = use_cases.create_building(dto).await;
200        assert!(result.is_ok());
201    }
202
203    #[tokio::test]
204    async fn test_create_building_validation_fails() {
205        let mock_repo = MockBuildingRepo::new();
206        let use_cases = BuildingUseCases::new(Arc::new(mock_repo));
207
208        let dto = CreateBuildingDto {
209            organization_id: Uuid::new_v4().to_string(),
210            name: "".to_string(), // Invalid: empty name
211            address: "123 Test St".to_string(),
212            city: "Paris".to_string(),
213            postal_code: "75001".to_string(),
214            country: "France".to_string(),
215            total_units: 10,
216            total_tantiemes: Some(1000),
217            construction_year: Some(2000),
218        };
219
220        let result = use_cases.create_building(dto).await;
221        assert!(result.is_err());
222    }
223}