koprogo_api/application/use_cases/
owner_use_cases.rs

1use crate::application::dto::{CreateOwnerDto, OwnerFilters, OwnerResponseDto, PageRequest};
2use crate::application::ports::OwnerRepository;
3use crate::domain::entities::Owner;
4use std::sync::Arc;
5use uuid::Uuid;
6
7pub struct OwnerUseCases {
8    repository: Arc<dyn OwnerRepository>,
9}
10
11impl OwnerUseCases {
12    pub fn new(repository: Arc<dyn OwnerRepository>) -> Self {
13        Self { repository }
14    }
15
16    pub async fn create_owner(&self, dto: CreateOwnerDto) -> Result<OwnerResponseDto, String> {
17        let organization_id = Uuid::parse_str(&dto.organization_id)
18            .map_err(|_| "Invalid organization_id format".to_string())?;
19
20        // Vérifier si l'email existe déjà
21        if (self.repository.find_by_email(&dto.email).await?).is_some() {
22            return Err("Email already exists".to_string());
23        }
24
25        let mut owner = Owner::new(
26            organization_id,
27            dto.first_name,
28            dto.last_name,
29            dto.email,
30            dto.phone,
31            dto.address,
32            dto.city,
33            dto.postal_code,
34            dto.country,
35        )?;
36
37        // Link to user account if provided
38        if let Some(user_id_str) = dto.user_id {
39            if !user_id_str.is_empty() {
40                owner.user_id = Some(
41                    Uuid::parse_str(&user_id_str)
42                        .map_err(|_| "Invalid user_id format".to_string())?,
43                );
44            }
45        }
46
47        let created = self.repository.create(&owner).await?;
48        Ok(self.to_response_dto(&created))
49    }
50
51    pub async fn get_owner(&self, id: Uuid) -> Result<Option<OwnerResponseDto>, String> {
52        let owner = self.repository.find_by_id(id).await?;
53        Ok(owner.map(|o| self.to_response_dto(&o)))
54    }
55
56    pub async fn find_owner_by_user_id(
57        &self,
58        user_id: Uuid,
59    ) -> Result<Option<OwnerResponseDto>, String> {
60        let owner = self.repository.find_by_user_id(user_id).await?;
61        Ok(owner.map(|o| self.to_response_dto(&o)))
62    }
63
64    pub async fn find_owner_by_user_id_and_organization(
65        &self,
66        user_id: Uuid,
67        organization_id: Uuid,
68    ) -> Result<Option<OwnerResponseDto>, String> {
69        let owner = self
70            .repository
71            .find_by_user_id_and_organization(user_id, organization_id)
72            .await?;
73        Ok(owner.map(|o| self.to_response_dto(&o)))
74    }
75
76    pub async fn list_owners(&self) -> Result<Vec<OwnerResponseDto>, String> {
77        let owners = self.repository.find_all().await?;
78        Ok(owners.iter().map(|o| self.to_response_dto(o)).collect())
79    }
80
81    pub async fn list_owners_paginated(
82        &self,
83        page_request: &PageRequest,
84        organization_id: Option<Uuid>,
85    ) -> Result<(Vec<OwnerResponseDto>, i64), String> {
86        let filters = OwnerFilters {
87            organization_id,
88            ..Default::default()
89        };
90
91        let (owners, total) = self
92            .repository
93            .find_all_paginated(page_request, &filters)
94            .await?;
95
96        let dtos = owners.iter().map(|o| self.to_response_dto(o)).collect();
97        Ok((dtos, total))
98    }
99
100    pub async fn update_owner(
101        &self,
102        id: Uuid,
103        first_name: String,
104        last_name: String,
105        email: String,
106        phone: Option<String>,
107    ) -> Result<OwnerResponseDto, String> {
108        // Get existing owner
109        let mut owner = self
110            .repository
111            .find_by_id(id)
112            .await?
113            .ok_or("Owner not found".to_string())?;
114
115        // Check if email is being changed and if the new email already exists
116        if owner.email != email {
117            if let Some(existing) = self.repository.find_by_email(&email).await? {
118                if existing.id != id {
119                    return Err("Email already exists".to_string());
120                }
121            }
122        }
123
124        // Update owner fields
125        owner.first_name = first_name;
126        owner.last_name = last_name;
127        owner.email = email;
128        owner.phone = phone;
129
130        // Save updated owner
131        let updated = self.repository.update(&owner).await?;
132        Ok(self.to_response_dto(&updated))
133    }
134
135    /// Link or unlink a user account to an owner.
136    /// Returns `Err` with a human-readable message if another owner is already linked
137    /// to the same user, or if the owner was not found.
138    pub async fn link_user_to_owner(
139        &self,
140        owner_id: Uuid,
141        user_id: Option<Uuid>,
142    ) -> Result<(), String> {
143        if let Some(uid) = user_id {
144            // Conflict check: is this user already linked to a different owner?
145            if let Some(existing) = self.repository.find_by_user_id(uid).await? {
146                if existing.id != owner_id {
147                    return Err(format!(
148                        "User is already linked to owner {} {} (ID: {})",
149                        existing.first_name, existing.last_name, existing.id
150                    ));
151                }
152            }
153        }
154
155        let updated = self.repository.set_user_link(owner_id, user_id).await?;
156        if !updated {
157            return Err("Owner not found".to_string());
158        }
159        Ok(())
160    }
161
162    fn to_response_dto(&self, owner: &Owner) -> OwnerResponseDto {
163        OwnerResponseDto {
164            id: owner.id.to_string(),
165            organization_id: owner.organization_id.to_string(),
166            user_id: owner.user_id.map(|id| id.to_string()),
167            first_name: owner.first_name.clone(),
168            last_name: owner.last_name.clone(),
169            email: owner.email.clone(),
170            phone: owner.phone.clone(),
171            address: owner.address.clone(),
172            city: owner.city.clone(),
173            postal_code: owner.postal_code.clone(),
174            country: owner.country.clone(),
175        }
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182    use async_trait::async_trait;
183    use std::collections::HashMap;
184    use std::sync::Mutex;
185
186    struct MockOwnerRepository {
187        items: Mutex<HashMap<Uuid, Owner>>,
188    }
189
190    impl MockOwnerRepository {
191        fn new() -> Self {
192            Self {
193                items: Mutex::new(HashMap::new()),
194            }
195        }
196    }
197
198    #[async_trait]
199    impl OwnerRepository for MockOwnerRepository {
200        async fn create(&self, owner: &Owner) -> Result<Owner, String> {
201            let mut items = self.items.lock().unwrap();
202            items.insert(owner.id, owner.clone());
203            Ok(owner.clone())
204        }
205
206        async fn find_by_id(&self, id: Uuid) -> Result<Option<Owner>, String> {
207            let items = self.items.lock().unwrap();
208            Ok(items.get(&id).cloned())
209        }
210
211        async fn find_by_user_id(&self, user_id: Uuid) -> Result<Option<Owner>, String> {
212            let items = self.items.lock().unwrap();
213            Ok(items.values().find(|o| o.user_id == Some(user_id)).cloned())
214        }
215
216        async fn find_by_user_id_and_organization(
217            &self,
218            user_id: Uuid,
219            organization_id: Uuid,
220        ) -> Result<Option<Owner>, String> {
221            let items = self.items.lock().unwrap();
222            Ok(items
223                .values()
224                .find(|o| o.user_id == Some(user_id) && o.organization_id == organization_id)
225                .cloned())
226        }
227
228        async fn find_by_email(&self, email: &str) -> Result<Option<Owner>, String> {
229            let items = self.items.lock().unwrap();
230            Ok(items.values().find(|o| o.email == email).cloned())
231        }
232
233        async fn find_all(&self) -> Result<Vec<Owner>, String> {
234            let items = self.items.lock().unwrap();
235            Ok(items.values().cloned().collect())
236        }
237
238        async fn find_all_paginated(
239            &self,
240            page_request: &PageRequest,
241            _filters: &OwnerFilters,
242        ) -> Result<(Vec<Owner>, i64), String> {
243            let items = self.items.lock().unwrap();
244            let all: Vec<Owner> = items.values().cloned().collect();
245            let total = all.len() as i64;
246            let offset = page_request.offset() as usize;
247            let limit = page_request.limit() as usize;
248            let page = all.into_iter().skip(offset).take(limit).collect();
249            Ok((page, total))
250        }
251
252        async fn update(&self, owner: &Owner) -> Result<Owner, String> {
253            let mut items = self.items.lock().unwrap();
254            items.insert(owner.id, owner.clone());
255            Ok(owner.clone())
256        }
257
258        async fn delete(&self, id: Uuid) -> Result<bool, String> {
259            let mut items = self.items.lock().unwrap();
260            Ok(items.remove(&id).is_some())
261        }
262
263        async fn set_user_link(
264            &self,
265            owner_id: Uuid,
266            user_id: Option<Uuid>,
267        ) -> Result<bool, String> {
268            let mut items = self.items.lock().unwrap();
269            if let Some(owner) = items.get_mut(&owner_id) {
270                owner.user_id = user_id;
271                Ok(true)
272            } else {
273                Ok(false)
274            }
275        }
276    }
277
278    fn make_use_cases(repo: MockOwnerRepository) -> OwnerUseCases {
279        OwnerUseCases::new(Arc::new(repo))
280    }
281
282    fn make_create_dto(org_id: Uuid) -> CreateOwnerDto {
283        CreateOwnerDto {
284            organization_id: org_id.to_string(),
285            first_name: "Jean".to_string(),
286            last_name: "Dupont".to_string(),
287            email: "jean.dupont@example.com".to_string(),
288            phone: Some("+32470123456".to_string()),
289            address: "Rue de la Loi 16".to_string(),
290            city: "Bruxelles".to_string(),
291            postal_code: "1000".to_string(),
292            country: "Belgique".to_string(),
293            user_id: None,
294        }
295    }
296
297    #[tokio::test]
298    async fn test_create_owner_success() {
299        let repo = MockOwnerRepository::new();
300        let use_cases = make_use_cases(repo);
301        let org_id = Uuid::new_v4();
302
303        let result = use_cases.create_owner(make_create_dto(org_id)).await;
304
305        assert!(result.is_ok());
306        let dto = result.unwrap();
307        assert_eq!(dto.first_name, "Jean");
308        assert_eq!(dto.last_name, "Dupont");
309        assert_eq!(dto.email, "jean.dupont@example.com");
310        assert_eq!(dto.organization_id, org_id.to_string());
311    }
312
313    #[tokio::test]
314    async fn test_create_owner_duplicate_email() {
315        let repo = MockOwnerRepository::new();
316        let org_id = Uuid::new_v4();
317
318        // Pre-populate with an existing owner having the same email
319        let existing = Owner::new(
320            org_id,
321            "Marie".to_string(),
322            "Martin".to_string(),
323            "jean.dupont@example.com".to_string(),
324            None,
325            "Av. Louise 100".to_string(),
326            "Bruxelles".to_string(),
327            "1050".to_string(),
328            "Belgique".to_string(),
329        )
330        .unwrap();
331        repo.items.lock().unwrap().insert(existing.id, existing);
332
333        let use_cases = make_use_cases(repo);
334        let result = use_cases.create_owner(make_create_dto(org_id)).await;
335
336        assert!(result.is_err());
337        assert_eq!(result.unwrap_err(), "Email already exists");
338    }
339
340    #[tokio::test]
341    async fn test_get_owner_found() {
342        let repo = MockOwnerRepository::new();
343        let org_id = Uuid::new_v4();
344        let owner = Owner::new(
345            org_id,
346            "Jean".to_string(),
347            "Dupont".to_string(),
348            "jean@example.com".to_string(),
349            None,
350            "Rue Test 1".to_string(),
351            "Bruxelles".to_string(),
352            "1000".to_string(),
353            "Belgique".to_string(),
354        )
355        .unwrap();
356        let owner_id = owner.id;
357        repo.items.lock().unwrap().insert(owner.id, owner);
358
359        let use_cases = make_use_cases(repo);
360        let result = use_cases.get_owner(owner_id).await;
361
362        assert!(result.is_ok());
363        let dto = result.unwrap();
364        assert!(dto.is_some());
365        assert_eq!(dto.unwrap().first_name, "Jean");
366    }
367
368    #[tokio::test]
369    async fn test_get_owner_not_found() {
370        let repo = MockOwnerRepository::new();
371        let use_cases = make_use_cases(repo);
372
373        let result = use_cases.get_owner(Uuid::new_v4()).await;
374
375        assert!(result.is_ok());
376        assert!(result.unwrap().is_none());
377    }
378
379    #[tokio::test]
380    async fn test_list_owners() {
381        let repo = MockOwnerRepository::new();
382        let org_id = Uuid::new_v4();
383
384        let owner1 = Owner::new(
385            org_id,
386            "Jean".to_string(),
387            "Dupont".to_string(),
388            "jean@example.com".to_string(),
389            None,
390            "Rue A".to_string(),
391            "Bruxelles".to_string(),
392            "1000".to_string(),
393            "Belgique".to_string(),
394        )
395        .unwrap();
396        let owner2 = Owner::new(
397            org_id,
398            "Marie".to_string(),
399            "Martin".to_string(),
400            "marie@example.com".to_string(),
401            None,
402            "Rue B".to_string(),
403            "Liege".to_string(),
404            "4000".to_string(),
405            "Belgique".to_string(),
406        )
407        .unwrap();
408
409        {
410            let mut items = repo.items.lock().unwrap();
411            items.insert(owner1.id, owner1);
412            items.insert(owner2.id, owner2);
413        }
414
415        let use_cases = make_use_cases(repo);
416        let result = use_cases.list_owners().await;
417
418        assert!(result.is_ok());
419        assert_eq!(result.unwrap().len(), 2);
420    }
421
422    #[tokio::test]
423    async fn test_update_owner_success() {
424        let repo = MockOwnerRepository::new();
425        let org_id = Uuid::new_v4();
426        let owner = Owner::new(
427            org_id,
428            "Jean".to_string(),
429            "Dupont".to_string(),
430            "jean@example.com".to_string(),
431            None,
432            "Rue Test".to_string(),
433            "Bruxelles".to_string(),
434            "1000".to_string(),
435            "Belgique".to_string(),
436        )
437        .unwrap();
438        let owner_id = owner.id;
439        repo.items.lock().unwrap().insert(owner.id, owner);
440
441        let use_cases = make_use_cases(repo);
442        let result = use_cases
443            .update_owner(
444                owner_id,
445                "Pierre".to_string(),
446                "Durand".to_string(),
447                "pierre@example.com".to_string(),
448                Some("+32470999999".to_string()),
449            )
450            .await;
451
452        assert!(result.is_ok());
453        let dto = result.unwrap();
454        assert_eq!(dto.first_name, "Pierre");
455        assert_eq!(dto.last_name, "Durand");
456        assert_eq!(dto.email, "pierre@example.com");
457        assert_eq!(dto.phone, Some("+32470999999".to_string()));
458    }
459
460    #[tokio::test]
461    async fn test_update_owner_email_conflict() {
462        let repo = MockOwnerRepository::new();
463        let org_id = Uuid::new_v4();
464
465        let owner1 = Owner::new(
466            org_id,
467            "Jean".to_string(),
468            "Dupont".to_string(),
469            "jean@example.com".to_string(),
470            None,
471            "Rue A".to_string(),
472            "Bruxelles".to_string(),
473            "1000".to_string(),
474            "Belgique".to_string(),
475        )
476        .unwrap();
477        let owner1_id = owner1.id;
478
479        let owner2 = Owner::new(
480            org_id,
481            "Marie".to_string(),
482            "Martin".to_string(),
483            "marie@example.com".to_string(),
484            None,
485            "Rue B".to_string(),
486            "Liege".to_string(),
487            "4000".to_string(),
488            "Belgique".to_string(),
489        )
490        .unwrap();
491
492        {
493            let mut items = repo.items.lock().unwrap();
494            items.insert(owner1.id, owner1);
495            items.insert(owner2.id, owner2);
496        }
497
498        let use_cases = make_use_cases(repo);
499
500        // Try to update owner1's email to owner2's email
501        let result = use_cases
502            .update_owner(
503                owner1_id,
504                "Jean".to_string(),
505                "Dupont".to_string(),
506                "marie@example.com".to_string(), // Already taken by owner2
507                None,
508            )
509            .await;
510
511        assert!(result.is_err());
512        assert_eq!(result.unwrap_err(), "Email already exists");
513    }
514}