koprogo_api/application/use_cases/
unit_use_cases.rs1use 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 let mut unit = self
77 .repository
78 .find_by_id(id)
79 .await?
80 .ok_or("Unit not found".to_string())?;
81
82 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 unit.validate_update()?;
92
93 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 let _unit = self
118 .repository
119 .find_by_id(id)
120 .await?
121 .ok_or("Unit not found".to_string())?;
122
123 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 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}