koprogo_api/application/use_cases/
gdpr_use_cases.rs

1use crate::application::dto::{GdprEraseResponseDto, GdprExportResponseDto};
2use crate::application::ports::{GdprRepository, UserRepository};
3use chrono::Utc;
4use std::sync::Arc;
5use uuid::Uuid;
6
7/// GDPR Use Cases for data export and erasure operations
8/// Implements business logic for GDPR Articles 15, 16, 17, 18, 21
9pub struct GdprUseCases {
10    gdpr_repository: Arc<dyn GdprRepository>,
11    user_repository: Arc<dyn UserRepository>,
12}
13
14impl GdprUseCases {
15    pub fn new(
16        gdpr_repository: Arc<dyn GdprRepository>,
17        user_repository: Arc<dyn UserRepository>,
18    ) -> Self {
19        Self {
20            gdpr_repository,
21            user_repository,
22        }
23    }
24
25    /// Export all personal data for a user (GDPR Article 15 - Right to Access)
26    ///
27    /// # Arguments
28    /// * `user_id` - UUID of the user requesting data export
29    /// * `requesting_user_id` - UUID of the user making the request (for authorization)
30    /// * `organization_id` - Optional organization scope (None for SuperAdmin)
31    ///
32    /// # Authorization
33    /// - Users can only export their own data
34    /// - SuperAdmin can export any user's data
35    ///
36    /// # Returns
37    /// * `Ok(GdprExportResponseDto)` - Complete data export in JSON format
38    /// * `Err(String)` - If user not found, not authorized, or database error
39    pub async fn export_user_data(
40        &self,
41        user_id: Uuid,
42        requesting_user_id: Uuid,
43        organization_id: Option<Uuid>,
44    ) -> Result<GdprExportResponseDto, String> {
45        // Authorization check: user can only export their own data
46        // SuperAdmin bypass is handled by passing organization_id = None
47        if user_id != requesting_user_id && organization_id.is_some() {
48            return Err("Unauthorized: You can only export your own data".to_string());
49        }
50
51        // Check if user is already anonymized
52        let is_anonymized = self.gdpr_repository.is_user_anonymized(user_id).await?;
53        if is_anonymized {
54            return Err("User data has been anonymized and cannot be exported".to_string());
55        }
56
57        // Aggregate all user data from database
58        let export = self
59            .gdpr_repository
60            .aggregate_user_data(user_id, organization_id)
61            .await?;
62
63        // Convert domain entity to DTO
64        Ok(GdprExportResponseDto::from(export))
65    }
66
67    /// Erase user data by anonymization (GDPR Article 17 - Right to Erasure)
68    ///
69    /// Anonymizes user account and linked owner profiles. Does not delete data entirely
70    /// to preserve referential integrity and comply with legal retention requirements
71    /// (e.g., financial records must be kept for 7 years in Belgium).
72    ///
73    /// # Arguments
74    /// * `user_id` - UUID of the user to anonymize
75    /// * `requesting_user_id` - UUID of the user making the request (for authorization)
76    /// * `organization_id` - Optional organization scope (None for SuperAdmin)
77    ///
78    /// # Authorization
79    /// - Users can only erase their own data
80    /// - SuperAdmin can erase any user's data
81    ///
82    /// # Returns
83    /// * `Ok(GdprEraseResponseDto)` - Anonymization confirmation
84    /// * `Err(String)` - If user not found, not authorized, already anonymized, or legal holds exist
85    pub async fn erase_user_data(
86        &self,
87        user_id: Uuid,
88        requesting_user_id: Uuid,
89        organization_id: Option<Uuid>,
90    ) -> Result<GdprEraseResponseDto, String> {
91        // Authorization check
92        if user_id != requesting_user_id && organization_id.is_some() {
93            return Err("Unauthorized: You can only erase your own data".to_string());
94        }
95
96        // Check if already anonymized
97        let is_anonymized = self.gdpr_repository.is_user_anonymized(user_id).await?;
98        if is_anonymized {
99            return Err("User data is already anonymized".to_string());
100        }
101
102        // Check for legal holds (e.g., unpaid expenses, ongoing legal proceedings)
103        let holds = self.gdpr_repository.check_legal_holds(user_id).await?;
104        if !holds.is_empty() {
105            return Err(format!(
106                "Cannot erase data due to legal holds: {}",
107                holds.join(", ")
108            ));
109        }
110
111        // Retrieve user data BEFORE anonymization (needed for email notification)
112        let user_data = self
113            .gdpr_repository
114            .aggregate_user_data(user_id, organization_id)
115            .await?;
116        let user_email = user_data.user_data.email.clone();
117        let user_first_name = user_data.user_data.first_name.clone();
118        let user_last_name = user_data.user_data.last_name.clone();
119
120        // Find all linked owner profiles
121        let owner_ids = self
122            .gdpr_repository
123            .find_owner_ids_by_user(user_id, organization_id)
124            .await?;
125
126        // Anonymize user account
127        self.gdpr_repository.anonymize_user(user_id).await?;
128
129        // Anonymize all linked owner profiles
130        let mut owners_anonymized = 0;
131        for owner_id in &owner_ids {
132            match self.gdpr_repository.anonymize_owner(*owner_id).await {
133                Ok(_) => owners_anonymized += 1,
134                Err(e) => {
135                    // Log error but continue (partial anonymization is acceptable)
136                    eprintln!("Warning: Failed to anonymize owner {}: {}", owner_id, e);
137                }
138            }
139        }
140
141        Ok(GdprEraseResponseDto {
142            success: true,
143            message: "Personal data has been successfully anonymized".to_string(),
144            anonymized_at: Utc::now().to_rfc3339(),
145            user_id: user_id.to_string(),
146            user_email,
147            user_first_name,
148            user_last_name,
149            owners_anonymized,
150        })
151    }
152
153    /// Check if user data can be erased (no legal holds)
154    ///
155    /// # Arguments
156    /// * `user_id` - UUID of the user to check
157    ///
158    /// # Returns
159    /// * `Ok(true)` - User can be erased
160    /// * `Ok(false)` - User has legal holds preventing erasure
161    /// * `Err(String)` - Database error
162    pub async fn can_erase_user(&self, user_id: Uuid) -> Result<bool, String> {
163        let holds = self.gdpr_repository.check_legal_holds(user_id).await?;
164        Ok(holds.is_empty())
165    }
166
167    /// Rectify user personal data (GDPR Article 16 - Right to Rectification)
168    ///
169    /// Allows users to correct inaccurate or incomplete personal data.
170    ///
171    /// # Arguments
172    /// * `user_id` - UUID of the user whose data to rectify
173    /// * `requesting_user_id` - UUID of the user making the request (for authorization)
174    /// * `email` - Optional new email address
175    /// * `first_name` - Optional new first name
176    /// * `last_name` - Optional new last name
177    ///
178    /// # Authorization
179    /// - Users can only rectify their own data
180    /// - SuperAdmin can rectify any user's data (organization_id = None)
181    ///
182    /// # Returns
183    /// * `Ok(User)` - Updated user entity
184    /// * `Err(String)` - If user not found, not authorized, or validation error
185    pub async fn rectify_user_data(
186        &self,
187        user_id: Uuid,
188        requesting_user_id: Uuid,
189        email: Option<String>,
190        first_name: Option<String>,
191        last_name: Option<String>,
192    ) -> Result<(), String> {
193        // Authorization check
194        if user_id != requesting_user_id {
195            // Only allow if SuperAdmin (checked by caller via organization_id)
196            return Err("Unauthorized: You can only rectify your own data".to_string());
197        }
198
199        // Fetch user
200        let mut user = self
201            .user_repository
202            .find_by_id(user_id)
203            .await?
204            .ok_or_else(|| format!("User not found: {}", user_id))?;
205
206        // Apply rectifications
207        user.rectify_data(email, first_name, last_name)?;
208
209        // Persist changes
210        self.user_repository.update(&user).await?;
211
212        Ok(())
213    }
214
215    /// Restrict data processing (GDPR Article 18 - Right to Restriction of Processing)
216    ///
217    /// Allows users to request temporary limitation of data processing.
218    /// When processing is restricted:
219    /// - Data is stored but not processed for certain operations
220    /// - Marketing communications are blocked
221    /// - Profiling/analytics are disabled
222    ///
223    /// # Arguments
224    /// * `user_id` - UUID of the user
225    /// * `requesting_user_id` - UUID of the user making the request (for authorization)
226    ///
227    /// # Authorization
228    /// - Users can only restrict their own data processing
229    ///
230    /// # Returns
231    /// * `Ok(())` - Processing restriction applied
232    /// * `Err(String)` - If user not found, not authorized, or already restricted
233    pub async fn restrict_user_processing(
234        &self,
235        user_id: Uuid,
236        requesting_user_id: Uuid,
237    ) -> Result<(), String> {
238        // Authorization check
239        if user_id != requesting_user_id {
240            return Err("Unauthorized: You can only restrict your own data processing".to_string());
241        }
242
243        // Fetch user
244        let mut user = self
245            .user_repository
246            .find_by_id(user_id)
247            .await?
248            .ok_or_else(|| format!("User not found: {}", user_id))?;
249
250        // Apply restriction
251        user.restrict_processing()?;
252
253        // Persist changes
254        self.user_repository.update(&user).await?;
255
256        Ok(())
257    }
258
259    /// Unrestrict data processing (Admin action or legal requirement met)
260    ///
261    /// # Arguments
262    /// * `user_id` - UUID of the user
263    /// * `admin_user_id` - UUID of the admin performing the action
264    ///
265    /// # Authorization
266    /// - Only admins/SuperAdmin can unrestrict processing
267    ///
268    /// # Returns
269    /// * `Ok(())` - Processing restriction removed
270    /// * `Err(String)` - If user not found
271    pub async fn unrestrict_user_processing(&self, user_id: Uuid) -> Result<(), String> {
272        // Fetch user
273        let mut user = self
274            .user_repository
275            .find_by_id(user_id)
276            .await?
277            .ok_or_else(|| format!("User not found: {}", user_id))?;
278
279        // Remove restriction
280        user.unrestrict_processing();
281
282        // Persist changes
283        self.user_repository.update(&user).await?;
284
285        Ok(())
286    }
287
288    /// Set marketing opt-out preference (GDPR Article 21 - Right to Object)
289    ///
290    /// Allows users to object to marketing communications and profiling.
291    ///
292    /// # Arguments
293    /// * `user_id` - UUID of the user
294    /// * `requesting_user_id` - UUID of the user making the request (for authorization)
295    /// * `opt_out` - true to opt out of marketing, false to opt back in
296    ///
297    /// # Authorization
298    /// - Users can only change their own marketing preferences
299    ///
300    /// # Returns
301    /// * `Ok(())` - Marketing preference updated
302    /// * `Err(String)` - If user not found or not authorized
303    pub async fn set_marketing_preference(
304        &self,
305        user_id: Uuid,
306        requesting_user_id: Uuid,
307        opt_out: bool,
308    ) -> Result<(), String> {
309        // Authorization check
310        if user_id != requesting_user_id {
311            return Err(
312                "Unauthorized: You can only change your own marketing preferences".to_string(),
313            );
314        }
315
316        // Fetch user
317        let mut user = self
318            .user_repository
319            .find_by_id(user_id)
320            .await?
321            .ok_or_else(|| format!("User not found: {}", user_id))?;
322
323        // Apply preference
324        user.set_marketing_opt_out(opt_out);
325
326        // Persist changes
327        self.user_repository.update(&user).await?;
328
329        Ok(())
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336    use crate::application::ports::gdpr_repository::MockGdprRepo;
337    use crate::application::ports::user_repository::MockUserRepo;
338    use crate::domain::entities::gdpr_export::{GdprExport, UserData};
339    use chrono::Utc;
340
341    fn create_test_user_data(user_id: Uuid) -> UserData {
342        UserData {
343            id: user_id,
344            email: "test@example.com".to_string(),
345            first_name: "John".to_string(),
346            last_name: "Doe".to_string(),
347            organization_id: Some(Uuid::new_v4()),
348            is_active: true,
349            is_anonymized: false,
350            created_at: Utc::now(),
351            updated_at: Utc::now(),
352        }
353    }
354
355    #[tokio::test]
356    async fn test_export_user_data_success() {
357        let user_id = Uuid::new_v4();
358        let org_id = Uuid::new_v4();
359
360        let mut mock_repo = MockGdprRepo::new();
361        mock_repo
362            .expect_is_user_anonymized()
363            .times(1)
364            .returning(|_| Ok(false));
365        mock_repo
366            .expect_aggregate_user_data()
367            .times(1)
368            .returning(move |_, _| {
369                let user_data = create_test_user_data(user_id);
370                Ok(GdprExport::new(user_data))
371            });
372
373        let mock_user_repo = MockUserRepo::new();
374
375        let use_cases = GdprUseCases::new(Arc::new(mock_repo), Arc::new(mock_user_repo));
376        let result = use_cases
377            .export_user_data(user_id, user_id, Some(org_id))
378            .await;
379
380        assert!(result.is_ok());
381        let dto = result.unwrap();
382        assert_eq!(dto.user.email, "test@example.com");
383    }
384
385    #[tokio::test]
386    async fn test_export_user_data_unauthorized() {
387        let user_id = Uuid::new_v4();
388        let other_user_id = Uuid::new_v4();
389        let org_id = Uuid::new_v4();
390
391        let mock_repo = MockGdprRepo::new();
392        let mock_user_repo = MockUserRepo::new();
393
394        let use_cases = GdprUseCases::new(Arc::new(mock_repo), Arc::new(mock_user_repo));
395
396        let result = use_cases
397            .export_user_data(user_id, other_user_id, Some(org_id))
398            .await;
399
400        assert!(result.is_err());
401        assert!(result
402            .unwrap_err()
403            .contains("Unauthorized: You can only export your own data"));
404    }
405
406    #[tokio::test]
407    async fn test_export_anonymized_user_fails() {
408        let user_id = Uuid::new_v4();
409
410        let mut mock_repo = MockGdprRepo::new();
411        mock_repo
412            .expect_is_user_anonymized()
413            .times(1)
414            .returning(|_| Ok(true));
415
416        let mock_user_repo = MockUserRepo::new();
417
418        let use_cases = GdprUseCases::new(Arc::new(mock_repo), Arc::new(mock_user_repo));
419        let result = use_cases
420            .export_user_data(user_id, user_id, Some(Uuid::new_v4()))
421            .await;
422
423        assert!(result.is_err());
424        assert!(result
425            .unwrap_err()
426            .contains("User data has been anonymized"));
427    }
428
429    #[tokio::test]
430    async fn test_erase_user_data_success() {
431        let user_id = Uuid::new_v4();
432        let owner_id1 = Uuid::new_v4();
433        let owner_id2 = Uuid::new_v4();
434        let org_id = Uuid::new_v4();
435
436        // Create test user data
437        let user_data = crate::domain::entities::gdpr_export::UserData {
438            id: user_id,
439            email: "test@example.com".to_string(),
440            first_name: "Test".to_string(),
441            last_name: "User".to_string(),
442            organization_id: Some(org_id),
443            is_active: true,
444            is_anonymized: false,
445            created_at: Utc::now(),
446            updated_at: Utc::now(),
447        };
448        let gdpr_export = crate::domain::entities::gdpr_export::GdprExport::new(user_data);
449
450        let mut mock_repo = MockGdprRepo::new();
451        mock_repo
452            .expect_is_user_anonymized()
453            .times(1)
454            .returning(|_| Ok(false));
455        mock_repo
456            .expect_check_legal_holds()
457            .times(1)
458            .returning(|_| Ok(vec![]));
459        mock_repo
460            .expect_aggregate_user_data()
461            .times(1)
462            .returning(move |_, _| Ok(gdpr_export.clone()));
463        mock_repo
464            .expect_find_owner_ids_by_user()
465            .times(1)
466            .returning(move |_, _| Ok(vec![owner_id1, owner_id2]));
467        mock_repo
468            .expect_anonymize_user()
469            .times(1)
470            .returning(|_| Ok(()));
471        mock_repo
472            .expect_anonymize_owner()
473            .times(2)
474            .returning(|_| Ok(()));
475
476        let mock_user_repo = MockUserRepo::new();
477
478        let use_cases = GdprUseCases::new(Arc::new(mock_repo), Arc::new(mock_user_repo));
479        let result = use_cases
480            .erase_user_data(user_id, user_id, Some(org_id))
481            .await;
482
483        assert!(result.is_ok());
484        let dto = result.unwrap();
485        assert!(dto.success);
486        assert_eq!(dto.owners_anonymized, 2);
487        assert_eq!(dto.user_email, "test@example.com");
488        assert_eq!(dto.user_first_name, "Test");
489        assert_eq!(dto.user_last_name, "User");
490    }
491
492    #[tokio::test]
493    async fn test_erase_user_data_unauthorized() {
494        let user_id = Uuid::new_v4();
495        let other_user_id = Uuid::new_v4();
496        let org_id = Uuid::new_v4();
497
498        let mock_repo = MockGdprRepo::new();
499        let mock_user_repo = MockUserRepo::new();
500
501        let use_cases = GdprUseCases::new(Arc::new(mock_repo), Arc::new(mock_user_repo));
502
503        let result = use_cases
504            .erase_user_data(user_id, other_user_id, Some(org_id))
505            .await;
506
507        assert!(result.is_err());
508        assert!(result
509            .unwrap_err()
510            .contains("Unauthorized: You can only erase your own data"));
511    }
512
513    #[tokio::test]
514    async fn test_erase_already_anonymized_user_fails() {
515        let user_id = Uuid::new_v4();
516
517        let mut mock_repo = MockGdprRepo::new();
518        mock_repo
519            .expect_is_user_anonymized()
520            .times(1)
521            .returning(|_| Ok(true));
522
523        let mock_user_repo = MockUserRepo::new();
524
525        let use_cases = GdprUseCases::new(Arc::new(mock_repo), Arc::new(mock_user_repo));
526        let result = use_cases
527            .erase_user_data(user_id, user_id, Some(Uuid::new_v4()))
528            .await;
529
530        assert!(result.is_err());
531        assert!(result.unwrap_err().contains("already anonymized"));
532    }
533
534    #[tokio::test]
535    async fn test_erase_with_legal_holds_fails() {
536        let user_id = Uuid::new_v4();
537
538        let mut mock_repo = MockGdprRepo::new();
539        mock_repo
540            .expect_is_user_anonymized()
541            .times(1)
542            .returning(|_| Ok(false));
543        mock_repo
544            .expect_check_legal_holds()
545            .times(1)
546            .returning(|_| Ok(vec!["Unpaid expenses".to_string()]));
547
548        let mock_user_repo = MockUserRepo::new();
549
550        let use_cases = GdprUseCases::new(Arc::new(mock_repo), Arc::new(mock_user_repo));
551        let result = use_cases
552            .erase_user_data(user_id, user_id, Some(Uuid::new_v4()))
553            .await;
554
555        assert!(result.is_err());
556        assert!(result.unwrap_err().contains("legal holds"));
557    }
558
559    #[tokio::test]
560    async fn test_can_erase_user_no_holds() {
561        let user_id = Uuid::new_v4();
562
563        let mut mock_repo = MockGdprRepo::new();
564        mock_repo
565            .expect_check_legal_holds()
566            .times(1)
567            .returning(|_| Ok(vec![]));
568
569        let mock_user_repo = MockUserRepo::new();
570
571        let use_cases = GdprUseCases::new(Arc::new(mock_repo), Arc::new(mock_user_repo));
572        let result = use_cases.can_erase_user(user_id).await;
573
574        assert!(result.is_ok());
575        assert!(result.unwrap());
576    }
577
578    #[tokio::test]
579    async fn test_can_erase_user_with_holds() {
580        let user_id = Uuid::new_v4();
581
582        let mut mock_repo = MockGdprRepo::new();
583        mock_repo
584            .expect_check_legal_holds()
585            .times(1)
586            .returning(|_| Ok(vec!["Unpaid expenses".to_string()]));
587
588        let mock_user_repo = MockUserRepo::new();
589
590        let use_cases = GdprUseCases::new(Arc::new(mock_repo), Arc::new(mock_user_repo));
591        let result = use_cases.can_erase_user(user_id).await;
592
593        assert!(result.is_ok());
594        assert!(!result.unwrap());
595    }
596}