koprogo_api/application/use_cases/
building_use_cases.rs1use crate::application::dto::{
2 BuildingFilters, BuildingResponseDto, CreateBuildingDto, PageRequest, UpdateBuildingDto,
3};
4use crate::application::error::AppError;
5use crate::application::ports::BuildingRepository;
6use crate::domain::entities::{Building, BuildingMetrics};
7use std::sync::Arc;
8use uuid::Uuid;
9
10pub struct BuildingUseCases {
11 repository: Arc<dyn BuildingRepository>,
12}
13
14impl BuildingUseCases {
15 pub fn new(repository: Arc<dyn BuildingRepository>) -> Self {
16 Self { repository }
17 }
18
19 pub async fn create_building(
20 &self,
21 dto: CreateBuildingDto,
22 ) -> Result<BuildingResponseDto, String> {
23 let acp_id =
28 Uuid::parse_str(&dto.acp_id).map_err(|_| "Invalid acp_id format".to_string())?;
29
30 let building = Building::new(
31 acp_id,
32 dto.name,
33 dto.address,
34 dto.city,
35 dto.postal_code,
36 dto.country,
37 dto.total_units,
38 dto.total_tantiemes.unwrap_or(1000),
39 dto.construction_year,
40 )?;
41
42 let created = self.repository.create(&building).await?;
43 Ok(self.to_response_dto(&created))
44 }
45
46 pub async fn get_building(&self, id: Uuid) -> Result<Option<BuildingResponseDto>, String> {
47 let building = self.repository.find_by_id(id).await?;
48 Ok(building.map(|b| self.to_response_dto(&b)))
49 }
50
51 pub async fn get_building_with_metrics(
57 &self,
58 id: Uuid,
59 ) -> Result<Option<BuildingResponseDto>, AppError> {
60 let pair = self
61 .repository
62 .find_by_id_with_metrics(id)
63 .await
64 .map_err(AppError::from)?;
65 Ok(pair.map(|(b, m)| Self::to_response_dto_with_metrics(&b, &m)))
66 }
67
68 pub async fn list_buildings(&self) -> Result<Vec<BuildingResponseDto>, String> {
69 let buildings = self.repository.find_all().await?;
70 Ok(buildings.iter().map(|b| self.to_response_dto(b)).collect())
71 }
72
73 pub async fn list_buildings_paginated(
74 &self,
75 page_request: &PageRequest,
76 organization_id: Option<Uuid>,
77 ) -> Result<(Vec<BuildingResponseDto>, i64), String> {
78 let filters = BuildingFilters {
79 organization_id,
80 ..Default::default()
81 };
82
83 let (buildings, total) = self
84 .repository
85 .find_all_paginated(page_request, &filters)
86 .await?;
87
88 let dtos = buildings.iter().map(|b| self.to_response_dto(b)).collect();
89 Ok((dtos, total))
90 }
91
92 pub async fn list_buildings_paginated_for_user(
95 &self,
96 page_request: &PageRequest,
97 organization_id: Option<Uuid>,
98 owner_user_id: Option<Uuid>,
99 search: Option<String>,
100 ) -> Result<(Vec<BuildingResponseDto>, i64), String> {
101 let filters = BuildingFilters {
102 organization_id,
103 owner_user_id,
104 search,
105 ..Default::default()
106 };
107
108 let (buildings, total) = self
109 .repository
110 .find_all_paginated(page_request, &filters)
111 .await?;
112
113 let dtos = buildings.iter().map(|b| self.to_response_dto(b)).collect();
114 Ok((dtos, total))
115 }
116
117 pub async fn update_building(
118 &self,
119 id: Uuid,
120 dto: UpdateBuildingDto,
121 ) -> Result<BuildingResponseDto, String> {
122 let mut building = self
123 .repository
124 .find_by_id(id)
125 .await?
126 .ok_or_else(|| "Building not found".to_string())?;
127
128 if let Some(acp_id_str) = dto.acp_id {
130 let acp_id =
131 Uuid::parse_str(&acp_id_str).map_err(|_| "Invalid acp_id format".to_string())?;
132 building.acp_id = acp_id;
133 }
134
135 building.update_info(
136 dto.name,
137 dto.address,
138 dto.city,
139 dto.postal_code,
140 dto.country,
141 dto.total_units,
142 dto.total_tantiemes.unwrap_or(1000),
143 dto.construction_year,
144 );
145
146 let updated = self.repository.update(&building).await?;
147 Ok(self.to_response_dto(&updated))
148 }
149
150 pub async fn delete_building(&self, id: Uuid) -> Result<bool, String> {
151 self.repository.delete(id).await
152 }
153
154 pub async fn find_by_slug(&self, slug: &str) -> Result<Option<Building>, String> {
156 self.repository.find_by_slug(slug).await
157 }
158
159 fn to_response_dto(&self, building: &Building) -> BuildingResponseDto {
160 Self::to_response_dto_with_metrics(building, &BuildingMetrics::empty())
165 }
166
167 fn to_response_dto_with_metrics(
173 building: &Building,
174 metrics: &BuildingMetrics,
175 ) -> BuildingResponseDto {
176 let is_conformant = building.is_conformant(metrics);
177 let delta = building.quota_delta(metrics);
180 BuildingResponseDto {
181 id: building.id.to_string(),
182 acp_id: building.acp_id.to_string(),
183 name: building.name.clone(),
184 address: building.address.clone(),
185 city: building.city.clone(),
186 postal_code: building.postal_code.clone(),
187 country: building.country.clone(),
188 total_units: building.total_units,
189 total_tantiemes: building.total_tantiemes,
190 construction_year: building.construction_year,
191 created_at: building.created_at.to_rfc3339(),
192 updated_at: building.updated_at.to_rfc3339(),
193 units_count: metrics.units_count,
194 quota_sum: metrics.quota_sum.to_string(),
195 is_conformant,
196 quota_delta: delta.to_string(),
197 }
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204 use crate::application::ports::BuildingRepository;
205 use async_trait::async_trait;
206 use mockall::mock;
207
208 mock! {
209 BuildingRepo {}
210
211 #[async_trait]
212 impl BuildingRepository for BuildingRepo {
213 async fn create(&self, building: &Building) -> Result<Building, String>;
214 async fn find_by_id(&self, id: Uuid) -> Result<Option<Building>, String>;
215 async fn find_all(&self) -> Result<Vec<Building>, String>;
216 async fn find_all_paginated(
217 &self,
218 page_request: &PageRequest,
219 filters: &BuildingFilters,
220 ) -> Result<(Vec<Building>, i64), String>;
221 async fn update(&self, building: &Building) -> Result<Building, String>;
222 async fn delete(&self, id: Uuid) -> Result<bool, String>;
223 async fn find_by_slug(&self, slug: &str) -> Result<Option<Building>, String>;
224 async fn find_by_id_with_metrics(
225 &self,
226 id: Uuid,
227 ) -> Result<Option<(Building, BuildingMetrics)>, String>;
228 }
229 }
230
231 #[tokio::test]
232 async fn test_create_building_success() {
233 let mut mock_repo = MockBuildingRepo::new();
234
235 mock_repo.expect_create().returning(|b| Ok(b.clone()));
236
237 let use_cases = BuildingUseCases::new(Arc::new(mock_repo));
238
239 let dto = CreateBuildingDto {
240 acp_id: Uuid::new_v4().to_string(),
241 name: "Test Building".to_string(),
242 address: "123 Test St".to_string(),
243 city: "Paris".to_string(),
244 postal_code: "75001".to_string(),
245 country: "France".to_string(),
246 total_units: 10,
247 total_tantiemes: Some(1000),
248 construction_year: Some(2000),
249 };
250
251 let result = use_cases.create_building(dto).await;
252 assert!(result.is_ok());
253 }
254
255 #[tokio::test]
256 async fn test_create_building_validation_fails() {
257 let mock_repo = MockBuildingRepo::new();
258 let use_cases = BuildingUseCases::new(Arc::new(mock_repo));
259
260 let dto = CreateBuildingDto {
261 acp_id: Uuid::new_v4().to_string(),
262 name: "".to_string(), address: "123 Test St".to_string(),
264 city: "Paris".to_string(),
265 postal_code: "75001".to_string(),
266 country: "France".to_string(),
267 total_units: 10,
268 total_tantiemes: Some(1000),
269 construction_year: Some(2000),
270 };
271
272 let result = use_cases.create_building(dto).await;
273 assert!(result.is_err());
274 }
275}