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