1use crate::application::dto::{BuildingFilters, BuildingResponseDto, PageRequest};
20use crate::application::error::AppError;
21use crate::application::ports::{AcpRepository, BuildingRepository, ListScope};
22use crate::domain::entities::Building;
23use std::sync::Arc;
24use uuid::Uuid;
25
26#[derive(Debug, Clone)]
32struct ResolvedScope {
33 organization_id: Option<Uuid>,
34 owner_user_id: Option<Uuid>,
35 all: bool,
37}
38
39pub struct ListBuildingsUseCase {
40 building_repo: Arc<dyn BuildingRepository>,
41 acp_repo: Arc<dyn AcpRepository>,
42}
43
44impl ListBuildingsUseCase {
45 pub fn new(
46 building_repo: Arc<dyn BuildingRepository>,
47 acp_repo: Arc<dyn AcpRepository>,
48 ) -> Self {
49 Self {
50 building_repo,
51 acp_repo,
52 }
53 }
54
55 pub async fn list_for_scope(
60 &self,
61 page_request: &PageRequest,
62 scope: ListScope,
63 ) -> Result<(Vec<BuildingResponseDto>, i64), AppError> {
64 let resolved = self.resolve(scope).await?;
65
66 let filters = BuildingFilters {
67 organization_id: if resolved.all {
68 None
69 } else {
70 resolved.organization_id
71 },
72 owner_user_id: resolved.owner_user_id,
73 ..Default::default()
74 };
75
76 let (buildings, total) = self
77 .building_repo
78 .find_all_paginated(page_request, &filters)
79 .await
80 .map_err(AppError::Database)?;
84
85 let dtos = buildings.iter().map(Self::to_response_dto).collect();
86 Ok((dtos, total))
87 }
88
89 async fn resolve(&self, scope: ListScope) -> Result<ResolvedScope, AppError> {
95 match scope {
96 ListScope::All => Ok(ResolvedScope {
97 organization_id: None,
98 owner_user_id: None,
99 all: true,
100 }),
101 ListScope::Organization(org_id) => Ok(ResolvedScope {
102 organization_id: Some(org_id),
103 owner_user_id: None,
104 all: false,
105 }),
106 ListScope::Owner(user_id) => Ok(ResolvedScope {
107 organization_id: None,
108 owner_user_id: Some(user_id),
109 all: false,
110 }),
111 }
112 }
113
114 pub async fn list_for_acp(
118 &self,
119 page_request: &PageRequest,
120 acp_id: Uuid,
121 ) -> Result<(Vec<BuildingResponseDto>, i64), AppError> {
122 let acp = self
123 .acp_repo
124 .find_by_id(acp_id)
125 .await?
126 .ok_or(AppError::AcpNotInScope { acp_id })?;
127
128 let org_id = match acp.organization_id {
132 Some(o) => o,
133 None => return Ok((vec![], 0)),
134 };
135
136 let filters = BuildingFilters {
137 organization_id: Some(org_id),
138 ..Default::default()
139 };
140 let (buildings, total) = self
141 .building_repo
142 .find_all_paginated(page_request, &filters)
143 .await
144 .map_err(AppError::Database)?;
145
146 let dtos = buildings.iter().map(Self::to_response_dto).collect();
147 Ok((dtos, total))
148 }
149
150 fn to_response_dto(b: &Building) -> BuildingResponseDto {
151 BuildingResponseDto {
160 id: b.id.to_string(),
161 acp_id: b.acp_id.to_string(),
162 name: b.name.clone(),
163 address: b.address.clone(),
164 city: b.city.clone(),
165 postal_code: b.postal_code.clone(),
166 country: b.country.clone(),
167 total_units: b.total_units,
168 total_tantiemes: b.total_tantiemes,
169 construction_year: b.construction_year,
170 created_at: b.created_at.to_rfc3339(),
171 updated_at: b.updated_at.to_rfc3339(),
172 units_count: 0,
173 quota_sum: String::from("0"),
174 is_conformant: false,
175 quota_delta: b.total_tantiemes.to_string(),
176 }
177 }
178}
179
180#[cfg(test)]
185mod tests {
186 use super::*;
187 use crate::application::ports::{AcpRepository, BuildingRepository, ListScope};
188 use crate::domain::entities::{Acp, Building};
189 use async_trait::async_trait;
190 use mockall::mock;
191
192 mock! {
193 BuildingRepo {}
194
195 #[async_trait]
196 impl BuildingRepository for BuildingRepo {
197 async fn create(&self, building: &Building) -> Result<Building, String>;
198 async fn find_by_id(&self, id: Uuid) -> Result<Option<Building>, String>;
199 async fn find_all(&self) -> Result<Vec<Building>, String>;
200 async fn find_all_paginated(
201 &self,
202 page_request: &PageRequest,
203 filters: &BuildingFilters,
204 ) -> Result<(Vec<Building>, i64), String>;
205 async fn update(&self, building: &Building) -> Result<Building, String>;
206 async fn delete(&self, id: Uuid) -> Result<bool, String>;
207 async fn find_by_slug(&self, slug: &str) -> Result<Option<Building>, String>;
208 async fn find_by_id_with_metrics(
209 &self,
210 id: Uuid,
211 ) -> Result<
212 Option<(Building, crate::domain::entities::BuildingMetrics)>,
213 String,
214 >;
215 }
216 }
217
218 mock! {
219 AcpRepo {}
220
221 #[async_trait]
222 impl AcpRepository for AcpRepo {
223 async fn create(&self, acp: &Acp) -> Result<Acp, AppError>;
224 async fn find_by_id(&self, id: Uuid) -> Result<Option<Acp>, AppError>;
225 async fn find_by_id_with_metrics(&self, id: Uuid) -> Result<Option<(Acp, crate::domain::entities::AcpMetrics)>, AppError>;
226 async fn list(&self, scope: ListScope) -> Result<Vec<Acp>, AppError>;
227 async fn list_with_metrics(
228 &self,
229 scope: ListScope,
230 ) -> Result<Vec<(Acp, crate::domain::entities::AcpMetrics)>, AppError>;
231 async fn update(&self, acp: &Acp) -> Result<Acp, AppError>;
232 async fn archive(&self, id: Uuid) -> Result<(), AppError>;
233 async fn count_buildings(&self, id: Uuid) -> Result<i64, AppError>;
234 }
235 }
236
237 fn page() -> PageRequest {
238 PageRequest {
239 page: 1,
240 per_page: 20,
241 sort_by: None,
242 order: crate::application::dto::SortOrder::default(),
243 }
244 }
245
246 fn make_building(org: Uuid) -> Building {
247 Building::new(
248 org,
249 "Building".to_string(),
250 "Addr".to_string(),
251 "City".to_string(),
252 "1000".to_string(),
253 "BE".to_string(),
254 10,
255 1000,
256 Some(2000),
257 )
258 .expect("valid building")
259 }
260
261 fn make_acp(org_id: Option<Uuid>) -> Acp {
262 Acp::new(
263 org_id,
264 "Acp".to_string(),
265 "Rue X".to_string(),
266 "1000".to_string(),
267 "Bruxelles".to_string(),
268 None,
269 )
270 .expect("valid acp")
271 }
272
273 #[tokio::test]
276 async fn happy_admin_sees_all_buildings() {
277 let mut building_repo = MockBuildingRepo::new();
278 let org = Uuid::new_v4();
279 let b = make_building(org);
280 let b_clone = b.clone();
281 building_repo
282 .expect_find_all_paginated()
283 .withf(|_pr, f| f.organization_id.is_none() && f.owner_user_id.is_none())
284 .returning(move |_, _| Ok((vec![b_clone.clone()], 1)));
285
286 let acp_repo = MockAcpRepo::new();
287 let uc = ListBuildingsUseCase::new(Arc::new(building_repo), Arc::new(acp_repo));
288 let (list, total) = uc
289 .list_for_scope(&page(), ListScope::All)
290 .await
291 .expect("ok");
292 assert_eq!(total, 1);
293 assert_eq!(list.len(), 1);
294 }
295
296 #[tokio::test]
297 async fn happy_syndic_sees_only_org_buildings() {
298 let org_a = Uuid::new_v4();
299 let mut building_repo = MockBuildingRepo::new();
300 let b = make_building(org_a);
301 let b_clone = b.clone();
302 building_repo
303 .expect_find_all_paginated()
304 .withf(move |_pr, f| f.organization_id == Some(org_a))
305 .returning(move |_, _| Ok((vec![b_clone.clone()], 1)));
306
307 let acp_repo = MockAcpRepo::new();
308 let uc = ListBuildingsUseCase::new(Arc::new(building_repo), Arc::new(acp_repo));
309 let (list, _) = uc
310 .list_for_scope(&page(), ListScope::Organization(org_a))
311 .await
312 .expect("ok");
313 assert_eq!(list.len(), 1);
314 }
315
316 #[tokio::test]
317 async fn happy_owner_sees_only_own_buildings() {
318 let user_id = Uuid::new_v4();
319 let mut building_repo = MockBuildingRepo::new();
320 building_repo
321 .expect_find_all_paginated()
322 .withf(move |_pr, f| f.owner_user_id == Some(user_id))
323 .returning(move |_, _| Ok((vec![], 0)));
324
325 let acp_repo = MockAcpRepo::new();
326 let uc = ListBuildingsUseCase::new(Arc::new(building_repo), Arc::new(acp_repo));
327 let (list, total) = uc
328 .list_for_scope(&page(), ListScope::Owner(user_id))
329 .await
330 .expect("ok");
331 assert!(list.is_empty());
332 assert_eq!(total, 0);
333 }
334
335 #[tokio::test]
338 async fn edge_list_for_acp_with_auto_managed_acp_returns_empty() {
339 let acp = make_acp(None);
340 let acp_id = acp.id;
341 let mut acp_repo = MockAcpRepo::new();
342 acp_repo
343 .expect_find_by_id()
344 .returning(move |_| Ok(Some(acp.clone())));
345
346 let building_repo = MockBuildingRepo::new();
347 let uc = ListBuildingsUseCase::new(Arc::new(building_repo), Arc::new(acp_repo));
348 let (list, total) = uc.list_for_acp(&page(), acp_id).await.expect("ok");
349 assert!(list.is_empty());
350 assert_eq!(total, 0);
351 }
352
353 #[tokio::test]
356 async fn security_list_for_unknown_acp_returns_acp_not_in_scope() {
357 let mut acp_repo = MockAcpRepo::new();
358 acp_repo.expect_find_by_id().returning(|_| Ok(None));
359 let building_repo = MockBuildingRepo::new();
360 let uc = ListBuildingsUseCase::new(Arc::new(building_repo), Arc::new(acp_repo));
361 let err = uc.list_for_acp(&page(), Uuid::new_v4()).await.unwrap_err();
362 assert!(matches!(err, AppError::AcpNotInScope { .. }));
363 }
364
365 #[tokio::test]
368 async fn negative_repository_error_maps_to_database_apperror() {
369 let mut building_repo = MockBuildingRepo::new();
370 building_repo
371 .expect_find_all_paginated()
372 .returning(|_, _| Err("connection lost".to_string()));
373 let acp_repo = MockAcpRepo::new();
374 let uc = ListBuildingsUseCase::new(Arc::new(building_repo), Arc::new(acp_repo));
375 let err = uc
376 .list_for_scope(&page(), ListScope::All)
377 .await
378 .unwrap_err();
379 match err {
380 AppError::Database(msg) => assert!(msg.contains("connection lost")),
381 other => panic!("expected Database, got {:?}", other),
382 }
383 }
384}