koprogo_api/application/ports/
gdpr_repository.rs

1use crate::domain::entities::gdpr_export::GdprExport;
2use async_trait::async_trait;
3use uuid::Uuid;
4
5/// GDPR Repository port for data export and anonymization operations
6/// Implements GDPR Article 15 (Right to Access) and Article 17 (Right to Erasure)
7#[async_trait]
8pub trait GdprRepository: Send + Sync {
9    /// Aggregate all personal data for a user (GDPR Article 15)
10    ///
11    /// Collects data from:
12    /// - Users table
13    /// - Owners table
14    /// - Unit ownership relationships
15    /// - Expenses
16    /// - Documents
17    /// - Meetings attendance
18    ///
19    /// # Arguments
20    /// * `user_id` - UUID of the user requesting data export
21    /// * `organization_id` - Optional organization scope (None for SuperAdmin)
22    ///
23    /// # Returns
24    /// * `Ok(GdprExport)` - Complete data export
25    /// * `Err(String)` - If user not found or database error
26    async fn aggregate_user_data(
27        &self,
28        user_id: Uuid,
29        organization_id: Option<Uuid>,
30    ) -> Result<GdprExport, String>;
31
32    /// Anonymize user account (GDPR Article 17)
33    ///
34    /// Replaces personal identifiable information with anonymized placeholders:
35    /// - email → anonymized-{uuid}@deleted.local
36    /// - first_name → "Anonymized"
37    /// - last_name → "User"
38    /// - Sets is_anonymized = true
39    /// - Sets anonymized_at = NOW()
40    ///
41    /// # Arguments
42    /// * `user_id` - UUID of the user to anonymize
43    ///
44    /// # Returns
45    /// * `Ok(())` - Anonymization successful
46    /// * `Err(String)` - If user not found, already anonymized, or database error
47    async fn anonymize_user(&self, user_id: Uuid) -> Result<(), String>;
48
49    /// Anonymize owner profile (GDPR Article 17)
50    ///
51    /// Replaces personal identifiable information:
52    /// - email → None
53    /// - phone → None
54    /// - address, city, postal_code, country → None
55    /// - first_name → "Anonymized"
56    /// - last_name → "User"
57    /// - Sets is_anonymized = true
58    /// - Sets anonymized_at = NOW()
59    ///
60    /// # Arguments
61    /// * `owner_id` - UUID of the owner to anonymize
62    ///
63    /// # Returns
64    /// * `Ok(())` - Anonymization successful
65    /// * `Err(String)` - If owner not found, already anonymized, or database error
66    async fn anonymize_owner(&self, owner_id: Uuid) -> Result<(), String>;
67
68    /// Find all owner IDs linked to a user
69    ///
70    /// Used to identify which owner profiles need anonymization when a user requests erasure.
71    ///
72    /// # Arguments
73    /// * `user_id` - UUID of the user
74    /// * `organization_id` - Optional organization scope
75    ///
76    /// # Returns
77    /// * `Ok(Vec<Uuid>)` - List of owner UUIDs
78    /// * `Err(String)` - Database error
79    async fn find_owner_ids_by_user(
80        &self,
81        user_id: Uuid,
82        organization_id: Option<Uuid>,
83    ) -> Result<Vec<Uuid>, String>;
84
85    /// Check if user has legal holds preventing deletion
86    ///
87    /// Verifies if user has outstanding financial obligations or legal requirements
88    /// that prevent complete anonymization (e.g., unpaid expenses, ongoing legal proceedings).
89    ///
90    /// # Arguments
91    /// * `user_id` - UUID of the user
92    ///
93    /// # Returns
94    /// * `Ok(Vec<String>)` - List of hold reasons (empty if no holds)
95    /// * `Err(String)` - Database error
96    async fn check_legal_holds(&self, user_id: Uuid) -> Result<Vec<String>, String>;
97
98    /// Check if user is already anonymized
99    ///
100    /// # Arguments
101    /// * `user_id` - UUID of the user
102    ///
103    /// # Returns
104    /// * `Ok(true)` - User is anonymized
105    /// * `Ok(false)` - User is not anonymized
106    /// * `Err(String)` - User not found or database error
107    async fn is_user_anonymized(&self, user_id: Uuid) -> Result<bool, String>;
108}
109
110// Mock implementation available for testing in use cases
111#[cfg(test)]
112pub use tests::MockGdprRepo;
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117    use async_trait::async_trait;
118    use mockall::mock;
119
120    // Mock implementation for testing
121    mock! {
122        pub GdprRepo {}
123
124        #[async_trait]
125        impl GdprRepository for GdprRepo {
126            async fn aggregate_user_data(
127                &self,
128                user_id: Uuid,
129                organization_id: Option<Uuid>,
130            ) -> Result<GdprExport, String>;
131
132            async fn anonymize_user(&self, user_id: Uuid) -> Result<(), String>;
133
134            async fn anonymize_owner(&self, owner_id: Uuid) -> Result<(), String>;
135
136            async fn find_owner_ids_by_user(
137                &self,
138                user_id: Uuid,
139                organization_id: Option<Uuid>,
140            ) -> Result<Vec<Uuid>, String>;
141
142            async fn check_legal_holds(&self, user_id: Uuid) -> Result<Vec<String>, String>;
143
144            async fn is_user_anonymized(&self, user_id: Uuid) -> Result<bool, String>;
145        }
146    }
147
148    #[tokio::test]
149    async fn test_mock_gdpr_repository() {
150        let mut mock_repo = MockGdprRepo::new();
151
152        // Test aggregate_user_data mock
153        let user_id = Uuid::new_v4();
154        mock_repo
155            .expect_aggregate_user_data()
156            .times(1)
157            .returning(|_, _| Err("Not implemented".to_string()));
158
159        let result = mock_repo.aggregate_user_data(user_id, None).await;
160        assert!(result.is_err());
161    }
162
163    #[tokio::test]
164    async fn test_mock_anonymize_user() {
165        let mut mock_repo = MockGdprRepo::new();
166
167        let user_id = Uuid::new_v4();
168        mock_repo
169            .expect_anonymize_user()
170            .times(1)
171            .returning(|_| Ok(()));
172
173        let result = mock_repo.anonymize_user(user_id).await;
174        assert!(result.is_ok());
175    }
176
177    #[tokio::test]
178    async fn test_mock_check_legal_holds() {
179        let mut mock_repo = MockGdprRepo::new();
180
181        let user_id = Uuid::new_v4();
182        mock_repo
183            .expect_check_legal_holds()
184            .times(1)
185            .returning(|_| Ok(vec!["Unpaid expenses".to_string()]));
186
187        let result = mock_repo.check_legal_holds(user_id).await;
188        assert_eq!(result.unwrap(), vec!["Unpaid expenses".to_string()]);
189    }
190
191    #[tokio::test]
192    async fn test_mock_is_user_anonymized() {
193        let mut mock_repo = MockGdprRepo::new();
194
195        let user_id = Uuid::new_v4();
196        mock_repo
197            .expect_is_user_anonymized()
198            .times(1)
199            .returning(|_| Ok(false));
200
201        let result = mock_repo.is_user_anonymized(user_id).await;
202        assert!(!result.unwrap());
203    }
204}