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    pub async fn update_building(
71        &self,
72        id: Uuid,
73        dto: UpdateBuildingDto,
74    ) -> Result<BuildingResponseDto, String> {
75        let mut building = self
76            .repository
77            .find_by_id(id)
78            .await?
79            .ok_or_else(|| "Building not found".to_string())?;
80
81        // Update organization if provided (SuperAdmin feature)
82        if let Some(org_id_str) = dto.organization_id {
83            let org_id = Uuid::parse_str(&org_id_str)
84                .map_err(|_| "Invalid organization_id format".to_string())?;
85            building.organization_id = org_id;
86        }
87
88        building.update_info(
89            dto.name,
90            dto.address,
91            dto.city,
92            dto.postal_code,
93            dto.country,
94            dto.total_units,
95            dto.total_tantiemes.unwrap_or(1000),
96            dto.construction_year,
97        );
98
99        let updated = self.repository.update(&building).await?;
100        Ok(self.to_response_dto(&updated))
101    }
102
103    pub async fn delete_building(&self, id: Uuid) -> Result<bool, String> {
104        self.repository.delete(id).await
105    }
106
107    /// Find building by URL slug (for public pages - Issue #92)
108    pub async fn find_by_slug(&self, slug: &str) -> Result<Option<Building>, String> {
109        self.repository.find_by_slug(slug).await
110    }
111
112    fn to_response_dto(&self, building: &Building) -> BuildingResponseDto {
113        BuildingResponseDto {
114            id: building.id.to_string(),
115            organization_id: building.organization_id.to_string(),
116            name: building.name.clone(),
117            address: building.address.clone(),
118            city: building.city.clone(),
119            postal_code: building.postal_code.clone(),
120            country: building.country.clone(),
121            total_units: building.total_units,
122            total_tantiemes: building.total_tantiemes,
123            construction_year: building.construction_year,
124            created_at: building.created_at.to_rfc3339(),
125            updated_at: building.updated_at.to_rfc3339(),
126        }
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use crate::application::ports::BuildingRepository;
134    use async_trait::async_trait;
135    use mockall::mock;
136
137    mock! {
138        BuildingRepo {}
139
140        #[async_trait]
141        impl BuildingRepository for BuildingRepo {
142            async fn create(&self, building: &Building) -> Result<Building, String>;
143            async fn find_by_id(&self, id: Uuid) -> Result<Option<Building>, String>;
144            async fn find_all(&self) -> Result<Vec<Building>, String>;
145            async fn find_all_paginated(
146                &self,
147                page_request: &PageRequest,
148                filters: &BuildingFilters,
149            ) -> Result<(Vec<Building>, i64), String>;
150            async fn update(&self, building: &Building) -> Result<Building, String>;
151            async fn delete(&self, id: Uuid) -> Result<bool, String>;
152            async fn find_by_slug(&self, slug: &str) -> Result<Option<Building>, String>;
153        }
154    }
155
156    #[tokio::test]
157    async fn test_create_building_success() {
158        let mut mock_repo = MockBuildingRepo::new();
159
160        mock_repo.expect_create().returning(|b| Ok(b.clone()));
161
162        let use_cases = BuildingUseCases::new(Arc::new(mock_repo));
163
164        let dto = CreateBuildingDto {
165            organization_id: Uuid::new_v4().to_string(),
166            name: "Test Building".to_string(),
167            address: "123 Test St".to_string(),
168            city: "Paris".to_string(),
169            postal_code: "75001".to_string(),
170            country: "France".to_string(),
171            total_units: 10,
172            total_tantiemes: Some(1000),
173            construction_year: Some(2000),
174        };
175
176        let result = use_cases.create_building(dto).await;
177        assert!(result.is_ok());
178    }
179
180    #[tokio::test]
181    async fn test_create_building_validation_fails() {
182        let mock_repo = MockBuildingRepo::new();
183        let use_cases = BuildingUseCases::new(Arc::new(mock_repo));
184
185        let dto = CreateBuildingDto {
186            organization_id: Uuid::new_v4().to_string(),
187            name: "".to_string(), // Invalid: empty name
188            address: "123 Test St".to_string(),
189            city: "Paris".to_string(),
190            postal_code: "75001".to_string(),
191            country: "France".to_string(),
192            total_units: 10,
193            total_tantiemes: Some(1000),
194            construction_year: Some(2000),
195        };
196
197        let result = use_cases.create_building(dto).await;
198        assert!(result.is_err());
199    }
200}