koprogo_api/application/use_cases/
consent_use_cases.rs

1use crate::application::dto::consent_dto::{ConsentRecordedResponse, ConsentStatusResponse};
2use crate::application::ports::audit_log_repository::AuditLogRepository;
3use crate::application::ports::consent_repository::ConsentRepository;
4use crate::domain::entities::consent::ConsentRecord;
5use crate::infrastructure::audit::{AuditEventType, AuditLogEntry};
6use std::sync::Arc;
7use uuid::Uuid;
8
9pub struct ConsentUseCases {
10    consent_repository: Arc<dyn ConsentRepository>,
11    audit_repository: Arc<dyn AuditLogRepository>,
12}
13
14impl ConsentUseCases {
15    pub fn new(
16        consent_repository: Arc<dyn ConsentRepository>,
17        audit_repository: Arc<dyn AuditLogRepository>,
18    ) -> Self {
19        Self {
20            consent_repository,
21            audit_repository,
22        }
23    }
24
25    /// Record user consent (GDPR Art. 7)
26    ///
27    /// Creates an immutable consent record with full audit trail.
28    pub async fn record_consent(
29        &self,
30        user_id: Uuid,
31        organization_id: Uuid,
32        consent_type: &str,
33        ip_address: Option<String>,
34        user_agent: Option<String>,
35        policy_version: Option<String>,
36    ) -> Result<ConsentRecordedResponse, String> {
37        // Create domain entity (validates consent_type)
38        let record = ConsentRecord::new(
39            user_id,
40            organization_id,
41            consent_type,
42            ip_address.clone(),
43            user_agent.clone(),
44            policy_version,
45        )?;
46
47        // Persist
48        let saved = self.consent_repository.create(&record).await?;
49
50        // Async audit log
51        let audit_entry = AuditLogEntry::new(
52            AuditEventType::ConsentRecorded,
53            Some(user_id),
54            Some(organization_id),
55        )
56        .with_resource("ConsentRecord", saved.id)
57        .with_client_info(ip_address, user_agent)
58        .with_metadata(serde_json::json!({
59            "consent_type": saved.consent_type,
60            "policy_version": saved.policy_version,
61        }));
62
63        let audit_repo = self.audit_repository.clone();
64        tokio::spawn(async move {
65            let _ = audit_repo.create(&audit_entry).await;
66        });
67
68        Ok(ConsentRecordedResponse {
69            message: format!("Consent for {} recorded successfully", saved.consent_type),
70            consent_type: saved.consent_type,
71            accepted_at: saved.accepted_at,
72            policy_version: saved.policy_version,
73        })
74    }
75
76    /// Get consent status for a user
77    ///
78    /// Returns summary of privacy policy and terms acceptance.
79    pub async fn get_consent_status(&self, user_id: Uuid) -> Result<ConsentStatusResponse, String> {
80        let status = self.consent_repository.get_consent_status(user_id).await?;
81
82        Ok(ConsentStatusResponse {
83            privacy_policy_accepted: status.privacy_policy_accepted,
84            terms_accepted: status.terms_accepted,
85            privacy_policy_accepted_at: status.privacy_policy_accepted_at,
86            terms_accepted_at: status.terms_accepted_at,
87            privacy_policy_version: status.privacy_policy_version,
88            terms_version: status.terms_version,
89        })
90    }
91
92    /// Check if user has accepted a specific consent type
93    pub async fn has_accepted(&self, user_id: Uuid, consent_type: &str) -> Result<bool, String> {
94        self.consent_repository
95            .has_accepted(user_id, consent_type)
96            .await
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use crate::application::dto::PageRequest;
104    use crate::application::ports::audit_log_repository::AuditLogFilters;
105    use crate::domain::entities::consent::ConsentStatus;
106    use async_trait::async_trait;
107    use std::sync::Mutex;
108
109    // Mock ConsentRepository
110    struct MockConsentRepository {
111        records: Mutex<Vec<ConsentRecord>>,
112    }
113
114    impl MockConsentRepository {
115        fn new() -> Self {
116            Self {
117                records: Mutex::new(Vec::new()),
118            }
119        }
120    }
121
122    #[async_trait]
123    impl ConsentRepository for MockConsentRepository {
124        async fn create(&self, record: &ConsentRecord) -> Result<ConsentRecord, String> {
125            let mut records = self.records.lock().unwrap();
126            records.push(record.clone());
127            Ok(record.clone())
128        }
129
130        async fn find_latest_by_user_and_type(
131            &self,
132            user_id: Uuid,
133            consent_type: &str,
134        ) -> Result<Option<ConsentRecord>, String> {
135            let records = self.records.lock().unwrap();
136            Ok(records
137                .iter()
138                .filter(|r| r.user_id == user_id && r.consent_type == consent_type)
139                .last()
140                .cloned())
141        }
142
143        async fn find_all_by_user(&self, user_id: Uuid) -> Result<Vec<ConsentRecord>, String> {
144            let records = self.records.lock().unwrap();
145            Ok(records
146                .iter()
147                .filter(|r| r.user_id == user_id)
148                .cloned()
149                .collect())
150        }
151
152        async fn has_accepted(&self, user_id: Uuid, consent_type: &str) -> Result<bool, String> {
153            let records = self.records.lock().unwrap();
154            Ok(records
155                .iter()
156                .any(|r| r.user_id == user_id && r.consent_type == consent_type))
157        }
158
159        async fn get_consent_status(&self, user_id: Uuid) -> Result<ConsentStatus, String> {
160            let records = self.records.lock().unwrap();
161            let privacy = records
162                .iter()
163                .filter(|r| r.user_id == user_id && r.consent_type == "privacy_policy")
164                .last()
165                .cloned();
166            let terms = records
167                .iter()
168                .filter(|r| r.user_id == user_id && r.consent_type == "terms")
169                .last()
170                .cloned();
171
172            Ok(ConsentStatus {
173                privacy_policy_accepted: privacy.is_some(),
174                terms_accepted: terms.is_some(),
175                privacy_policy_accepted_at: privacy.as_ref().map(|r| r.accepted_at),
176                terms_accepted_at: terms.as_ref().map(|r| r.accepted_at),
177                privacy_policy_version: privacy.map(|r| r.policy_version),
178                terms_version: terms.map(|r| r.policy_version),
179            })
180        }
181    }
182
183    // Mock AuditLogRepository
184    struct MockAuditLogRepository;
185
186    #[async_trait]
187    impl AuditLogRepository for MockAuditLogRepository {
188        async fn create(&self, _entry: &AuditLogEntry) -> Result<AuditLogEntry, String> {
189            Ok(AuditLogEntry::new(
190                AuditEventType::ConsentRecorded,
191                None,
192                None,
193            ))
194        }
195        async fn find_by_id(&self, _id: Uuid) -> Result<Option<AuditLogEntry>, String> {
196            Ok(None)
197        }
198        async fn find_all_paginated(
199            &self,
200            _page_request: &PageRequest,
201            _filters: &AuditLogFilters,
202        ) -> Result<(Vec<AuditLogEntry>, i64), String> {
203            Ok((vec![], 0))
204        }
205        async fn find_recent(&self, _limit: i64) -> Result<Vec<AuditLogEntry>, String> {
206            Ok(vec![])
207        }
208        async fn find_failed_operations(
209            &self,
210            _page_request: &PageRequest,
211            _organization_id: Option<Uuid>,
212        ) -> Result<(Vec<AuditLogEntry>, i64), String> {
213            Ok((vec![], 0))
214        }
215        async fn delete_older_than(
216            &self,
217            _timestamp: chrono::DateTime<chrono::Utc>,
218        ) -> Result<i64, String> {
219            Ok(0)
220        }
221        async fn count_by_filters(&self, _filters: &AuditLogFilters) -> Result<i64, String> {
222            Ok(0)
223        }
224    }
225
226    fn make_use_cases() -> ConsentUseCases {
227        ConsentUseCases::new(
228            Arc::new(MockConsentRepository::new()),
229            Arc::new(MockAuditLogRepository),
230        )
231    }
232
233    #[tokio::test]
234    async fn test_record_privacy_policy_consent() {
235        let uc = make_use_cases();
236        let user_id = Uuid::new_v4();
237        let org_id = Uuid::new_v4();
238
239        let result = uc
240            .record_consent(
241                user_id,
242                org_id,
243                "privacy_policy",
244                None,
245                None,
246                Some("1.0".to_string()),
247            )
248            .await;
249
250        assert!(result.is_ok());
251        let response = result.unwrap();
252        assert_eq!(response.consent_type, "privacy_policy");
253        assert_eq!(response.policy_version, "1.0");
254        assert!(response.message.contains("privacy_policy"));
255    }
256
257    #[tokio::test]
258    async fn test_record_terms_consent() {
259        let uc = make_use_cases();
260        let result = uc
261            .record_consent(Uuid::new_v4(), Uuid::new_v4(), "terms", None, None, None)
262            .await;
263
264        assert!(result.is_ok());
265        assert_eq!(result.unwrap().consent_type, "terms");
266    }
267
268    #[tokio::test]
269    async fn test_record_invalid_consent_type() {
270        let uc = make_use_cases();
271        let result = uc
272            .record_consent(Uuid::new_v4(), Uuid::new_v4(), "invalid", None, None, None)
273            .await;
274
275        assert!(result.is_err());
276        assert!(result.unwrap_err().contains("Invalid consent type"));
277    }
278
279    #[tokio::test]
280    async fn test_get_consent_status_empty() {
281        let uc = make_use_cases();
282        let result = uc.get_consent_status(Uuid::new_v4()).await;
283
284        assert!(result.is_ok());
285        let status = result.unwrap();
286        assert!(!status.privacy_policy_accepted);
287        assert!(!status.terms_accepted);
288    }
289
290    #[tokio::test]
291    async fn test_get_consent_status_after_recording() {
292        let uc = make_use_cases();
293        let user_id = Uuid::new_v4();
294        let org_id = Uuid::new_v4();
295
296        uc.record_consent(user_id, org_id, "privacy_policy", None, None, None)
297            .await
298            .unwrap();
299
300        let status = uc.get_consent_status(user_id).await.unwrap();
301        assert!(status.privacy_policy_accepted);
302        assert!(!status.terms_accepted);
303    }
304
305    #[tokio::test]
306    async fn test_has_accepted() {
307        let uc = make_use_cases();
308        let user_id = Uuid::new_v4();
309        let org_id = Uuid::new_v4();
310
311        assert!(!uc.has_accepted(user_id, "terms").await.unwrap());
312
313        uc.record_consent(user_id, org_id, "terms", None, None, None)
314            .await
315            .unwrap();
316
317        assert!(uc.has_accepted(user_id, "terms").await.unwrap());
318    }
319}