koprogo_api/application/use_cases/
unit_use_cases.rs

1use crate::application::dto::{
2    CreateUnitDto, PageRequest, UnitFilters, UnitResponseDto, UpdateUnitDto,
3};
4use crate::application::ports::UnitRepository;
5use crate::domain::entities::Unit;
6use std::sync::Arc;
7use uuid::Uuid;
8
9pub struct UnitUseCases {
10    repository: Arc<dyn UnitRepository>,
11}
12
13impl UnitUseCases {
14    pub fn new(repository: Arc<dyn UnitRepository>) -> Self {
15        Self { repository }
16    }
17
18    pub async fn create_unit(&self, dto: CreateUnitDto) -> Result<UnitResponseDto, String> {
19        let organization_id = Uuid::parse_str(&dto.organization_id)
20            .map_err(|_| "Invalid organization_id format".to_string())?;
21        let building_id = Uuid::parse_str(&dto.building_id)
22            .map_err(|_| "Invalid building ID format".to_string())?;
23
24        let unit = Unit::new(
25            organization_id,
26            building_id,
27            dto.unit_number,
28            dto.unit_type,
29            dto.floor,
30            dto.surface_area,
31            dto.quota,
32        )?;
33
34        let created = self.repository.create(&unit).await?;
35        Ok(self.to_response_dto(&created))
36    }
37
38    pub async fn get_unit(&self, id: Uuid) -> Result<Option<UnitResponseDto>, String> {
39        let unit = self.repository.find_by_id(id).await?;
40        Ok(unit.map(|u| self.to_response_dto(&u)))
41    }
42
43    pub async fn list_units_by_building(
44        &self,
45        building_id: Uuid,
46    ) -> Result<Vec<UnitResponseDto>, String> {
47        let units = self.repository.find_by_building(building_id).await?;
48        Ok(units.iter().map(|u| self.to_response_dto(u)).collect())
49    }
50
51    pub async fn list_units_paginated(
52        &self,
53        page_request: &PageRequest,
54        organization_id: Option<Uuid>,
55    ) -> Result<(Vec<UnitResponseDto>, i64), String> {
56        let filters = UnitFilters {
57            organization_id,
58            ..Default::default()
59        };
60
61        let (units, total) = self
62            .repository
63            .find_all_paginated(page_request, &filters)
64            .await?;
65
66        let dtos = units.iter().map(|u| self.to_response_dto(u)).collect();
67        Ok((dtos, total))
68    }
69
70    pub async fn update_unit(
71        &self,
72        id: Uuid,
73        dto: UpdateUnitDto,
74    ) -> Result<UnitResponseDto, String> {
75        // Get existing unit
76        let mut unit = self
77            .repository
78            .find_by_id(id)
79            .await?
80            .ok_or("Unit not found".to_string())?;
81
82        // Update unit fields
83        unit.unit_number = dto.unit_number;
84        unit.unit_type = dto.unit_type;
85        unit.floor = Some(dto.floor);
86        unit.surface_area = dto.surface_area;
87        unit.quota = dto.quota;
88        unit.updated_at = chrono::Utc::now();
89
90        // Validate the updated unit
91        unit.validate_update()?;
92
93        // Save updated unit
94        let updated = self.repository.update(&unit).await?;
95        Ok(self.to_response_dto(&updated))
96    }
97
98    pub async fn assign_owner(
99        &self,
100        unit_id: Uuid,
101        owner_id: Uuid,
102    ) -> Result<UnitResponseDto, String> {
103        let mut unit = self
104            .repository
105            .find_by_id(unit_id)
106            .await?
107            .ok_or_else(|| "Unit not found".to_string())?;
108
109        unit.assign_owner(owner_id);
110
111        let updated = self.repository.update(&unit).await?;
112        Ok(self.to_response_dto(&updated))
113    }
114
115    pub async fn delete_unit(&self, id: Uuid) -> Result<bool, String> {
116        // Check if unit exists
117        let _unit = self
118            .repository
119            .find_by_id(id)
120            .await?
121            .ok_or("Unit not found".to_string())?;
122
123        // Delete the unit
124        self.repository.delete(id).await
125    }
126
127    fn to_response_dto(&self, unit: &Unit) -> UnitResponseDto {
128        UnitResponseDto {
129            id: unit.id.to_string(),
130            building_id: unit.building_id.to_string(),
131            unit_number: unit.unit_number.clone(),
132            unit_type: unit.unit_type.clone(),
133            floor: unit.floor,
134            surface_area: unit.surface_area,
135            quota: unit.quota,
136            owner_id: unit.owner_id.map(|id| id.to_string()),
137        }
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use crate::domain::entities::UnitType;
145    use async_trait::async_trait;
146    use std::collections::HashMap;
147    use std::sync::Mutex;
148
149    struct MockUnitRepository {
150        items: Mutex<HashMap<Uuid, Unit>>,
151    }
152
153    impl MockUnitRepository {
154        fn new() -> Self {
155            Self {
156                items: Mutex::new(HashMap::new()),
157            }
158        }
159    }
160
161    #[async_trait]
162    impl UnitRepository for MockUnitRepository {
163        async fn create(&self, unit: &Unit) -> Result<Unit, String> {
164            let mut items = self.items.lock().unwrap();
165            items.insert(unit.id, unit.clone());
166            Ok(unit.clone())
167        }
168
169        async fn find_by_id(&self, id: Uuid) -> Result<Option<Unit>, String> {
170            let items = self.items.lock().unwrap();
171            Ok(items.get(&id).cloned())
172        }
173
174        async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<Unit>, String> {
175            let items = self.items.lock().unwrap();
176            Ok(items
177                .values()
178                .filter(|u| u.building_id == building_id)
179                .cloned()
180                .collect())
181        }
182
183        async fn find_by_owner(&self, owner_id: Uuid) -> Result<Vec<Unit>, String> {
184            let items = self.items.lock().unwrap();
185            Ok(items
186                .values()
187                .filter(|u| u.owner_id == Some(owner_id))
188                .cloned()
189                .collect())
190        }
191
192        async fn find_all_paginated(
193            &self,
194            page_request: &PageRequest,
195            _filters: &UnitFilters,
196        ) -> Result<(Vec<Unit>, i64), String> {
197            let items = self.items.lock().unwrap();
198            let all: Vec<Unit> = items.values().cloned().collect();
199            let total = all.len() as i64;
200            let offset = page_request.offset() as usize;
201            let limit = page_request.limit() as usize;
202            let page = all.into_iter().skip(offset).take(limit).collect();
203            Ok((page, total))
204        }
205
206        async fn update(&self, unit: &Unit) -> Result<Unit, String> {
207            let mut items = self.items.lock().unwrap();
208            items.insert(unit.id, unit.clone());
209            Ok(unit.clone())
210        }
211
212        async fn delete(&self, id: Uuid) -> Result<bool, String> {
213            let mut items = self.items.lock().unwrap();
214            Ok(items.remove(&id).is_some())
215        }
216    }
217
218    fn make_use_cases(repo: MockUnitRepository) -> UnitUseCases {
219        UnitUseCases::new(Arc::new(repo))
220    }
221
222    fn make_create_dto(org_id: Uuid, building_id: Uuid) -> CreateUnitDto {
223        CreateUnitDto {
224            organization_id: org_id.to_string(),
225            building_id: building_id.to_string(),
226            unit_number: "A101".to_string(),
227            unit_type: UnitType::Apartment,
228            floor: Some(1),
229            surface_area: 85.0,
230            quota: 50.0,
231        }
232    }
233
234    #[tokio::test]
235    async fn test_create_unit_success() {
236        let repo = MockUnitRepository::new();
237        let use_cases = make_use_cases(repo);
238        let org_id = Uuid::new_v4();
239        let building_id = Uuid::new_v4();
240
241        let result = use_cases
242            .create_unit(make_create_dto(org_id, building_id))
243            .await;
244
245        assert!(result.is_ok());
246        let dto = result.unwrap();
247        assert_eq!(dto.unit_number, "A101");
248        assert_eq!(dto.surface_area, 85.0);
249        assert_eq!(dto.quota, 50.0);
250        assert_eq!(dto.building_id, building_id.to_string());
251        assert!(dto.owner_id.is_none());
252    }
253
254    #[tokio::test]
255    async fn test_create_unit_invalid_building_id() {
256        let repo = MockUnitRepository::new();
257        let use_cases = make_use_cases(repo);
258        let org_id = Uuid::new_v4();
259
260        let dto = CreateUnitDto {
261            organization_id: org_id.to_string(),
262            building_id: "not-a-valid-uuid".to_string(),
263            unit_number: "A101".to_string(),
264            unit_type: UnitType::Apartment,
265            floor: Some(1),
266            surface_area: 85.0,
267            quota: 50.0,
268        };
269
270        let result = use_cases.create_unit(dto).await;
271
272        assert!(result.is_err());
273        assert_eq!(result.unwrap_err(), "Invalid building ID format");
274    }
275
276    #[tokio::test]
277    async fn test_get_unit() {
278        let repo = MockUnitRepository::new();
279        let org_id = Uuid::new_v4();
280        let building_id = Uuid::new_v4();
281        let unit = Unit::new(
282            org_id,
283            building_id,
284            "B202".to_string(),
285            UnitType::Parking,
286            Some(-1),
287            15.0,
288            10.0,
289        )
290        .unwrap();
291        let unit_id = unit.id;
292        repo.items.lock().unwrap().insert(unit.id, unit);
293
294        let use_cases = make_use_cases(repo);
295        let result = use_cases.get_unit(unit_id).await;
296
297        assert!(result.is_ok());
298        let dto = result.unwrap();
299        assert!(dto.is_some());
300        let dto = dto.unwrap();
301        assert_eq!(dto.unit_number, "B202");
302        assert_eq!(dto.surface_area, 15.0);
303    }
304
305    #[tokio::test]
306    async fn test_list_units_by_building() {
307        let repo = MockUnitRepository::new();
308        let org_id = Uuid::new_v4();
309        let building_a = Uuid::new_v4();
310        let building_b = Uuid::new_v4();
311
312        let unit1 = Unit::new(
313            org_id,
314            building_a,
315            "A101".to_string(),
316            UnitType::Apartment,
317            Some(1),
318            80.0,
319            40.0,
320        )
321        .unwrap();
322        let unit2 = Unit::new(
323            org_id,
324            building_a,
325            "A102".to_string(),
326            UnitType::Apartment,
327            Some(1),
328            65.0,
329            30.0,
330        )
331        .unwrap();
332        let unit3 = Unit::new(
333            org_id,
334            building_b,
335            "B101".to_string(),
336            UnitType::Commercial,
337            Some(0),
338            120.0,
339            100.0,
340        )
341        .unwrap();
342
343        {
344            let mut items = repo.items.lock().unwrap();
345            items.insert(unit1.id, unit1);
346            items.insert(unit2.id, unit2);
347            items.insert(unit3.id, unit3);
348        }
349
350        let use_cases = make_use_cases(repo);
351        let result = use_cases.list_units_by_building(building_a).await;
352
353        assert!(result.is_ok());
354        let units = result.unwrap();
355        assert_eq!(units.len(), 2);
356        assert!(units
357            .iter()
358            .all(|u| u.building_id == building_a.to_string()));
359    }
360
361    #[tokio::test]
362    async fn test_delete_unit() {
363        let repo = MockUnitRepository::new();
364        let org_id = Uuid::new_v4();
365        let building_id = Uuid::new_v4();
366        let unit = Unit::new(
367            org_id,
368            building_id,
369            "A101".to_string(),
370            UnitType::Apartment,
371            Some(1),
372            80.0,
373            50.0,
374        )
375        .unwrap();
376        let unit_id = unit.id;
377        repo.items.lock().unwrap().insert(unit.id, unit);
378
379        let use_cases = make_use_cases(repo);
380        let result = use_cases.delete_unit(unit_id).await;
381
382        assert!(result.is_ok());
383        assert!(result.unwrap());
384
385        // Verify it is gone
386        let get_result = use_cases.get_unit(unit_id).await;
387        assert!(get_result.is_ok());
388        assert!(get_result.unwrap().is_none());
389    }
390
391    #[tokio::test]
392    async fn test_assign_owner() {
393        let repo = MockUnitRepository::new();
394        let org_id = Uuid::new_v4();
395        let building_id = Uuid::new_v4();
396        let unit = Unit::new(
397            org_id,
398            building_id,
399            "A101".to_string(),
400            UnitType::Apartment,
401            Some(1),
402            80.0,
403            50.0,
404        )
405        .unwrap();
406        let unit_id = unit.id;
407        repo.items.lock().unwrap().insert(unit.id, unit);
408
409        let use_cases = make_use_cases(repo);
410        let owner_id = Uuid::new_v4();
411        let result = use_cases.assign_owner(unit_id, owner_id).await;
412
413        assert!(result.is_ok());
414        let dto = result.unwrap();
415        assert_eq!(dto.owner_id, Some(owner_id.to_string()));
416    }
417}