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}