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                .rfind(|r| r.user_id == user_id && r.consent_type == consent_type)
139                .cloned())
140        }
141
142        async fn find_all_by_user(&self, user_id: Uuid) -> Result<Vec<ConsentRecord>, String> {
143            let records = self.records.lock().unwrap();
144            Ok(records
145                .iter()
146                .filter(|r| r.user_id == user_id)
147                .cloned()
148                .collect())
149        }
150
151        async fn has_accepted(&self, user_id: Uuid, consent_type: &str) -> Result<bool, String> {
152            let records = self.records.lock().unwrap();
153            Ok(records
154                .iter()
155                .any(|r| r.user_id == user_id && r.consent_type == consent_type))
156        }
157
158        async fn get_consent_status(&self, user_id: Uuid) -> Result<ConsentStatus, String> {
159            let records = self.records.lock().unwrap();
160            let privacy = records
161                .iter()
162                .rfind(|r| r.user_id == user_id && r.consent_type == "privacy_policy")
163                .cloned();
164            let terms = records
165                .iter()
166                .rfind(|r| r.user_id == user_id && r.consent_type == "terms")
167                .cloned();
168
169            Ok(ConsentStatus {
170                privacy_policy_accepted: privacy.is_some(),
171                terms_accepted: terms.is_some(),
172                privacy_policy_accepted_at: privacy.as_ref().map(|r| r.accepted_at),
173                terms_accepted_at: terms.as_ref().map(|r| r.accepted_at),
174                privacy_policy_version: privacy.map(|r| r.policy_version),
175                terms_version: terms.map(|r| r.policy_version),
176            })
177        }
178    }
179
180    // Mock AuditLogRepository
181    struct MockAuditLogRepository;
182
183    #[async_trait]
184    impl AuditLogRepository for MockAuditLogRepository {
185        async fn create(&self, _entry: &AuditLogEntry) -> Result<AuditLogEntry, String> {
186            Ok(AuditLogEntry::new(
187                AuditEventType::ConsentRecorded,
188                None,
189                None,
190            ))
191        }
192        async fn find_by_id(&self, _id: Uuid) -> Result<Option<AuditLogEntry>, String> {
193            Ok(None)
194        }
195        async fn find_all_paginated(
196            &self,
197            _page_request: &PageRequest,
198            _filters: &AuditLogFilters,
199        ) -> Result<(Vec<AuditLogEntry>, i64), String> {
200            Ok((vec![], 0))
201        }
202        async fn find_recent(&self, _limit: i64) -> Result<Vec<AuditLogEntry>, String> {
203            Ok(vec![])
204        }
205        async fn find_failed_operations(
206            &self,
207            _page_request: &PageRequest,
208            _organization_id: Option<Uuid>,
209        ) -> Result<(Vec<AuditLogEntry>, i64), String> {
210            Ok((vec![], 0))
211        }
212        async fn delete_older_than(
213            &self,
214            _timestamp: chrono::DateTime<chrono::Utc>,
215        ) -> Result<i64, String> {
216            Ok(0)
217        }
218        async fn count_by_filters(&self, _filters: &AuditLogFilters) -> Result<i64, String> {
219            Ok(0)
220        }
221    }
222
223    fn make_use_cases() -> ConsentUseCases {
224        ConsentUseCases::new(
225            Arc::new(MockConsentRepository::new()),
226            Arc::new(MockAuditLogRepository),
227        )
228    }
229
230    #[tokio::test]
231    async fn test_record_privacy_policy_consent() {
232        let uc = make_use_cases();
233        let user_id = Uuid::new_v4();
234        let org_id = Uuid::new_v4();
235
236        let result = uc
237            .record_consent(
238                user_id,
239                org_id,
240                "privacy_policy",
241                None,
242                None,
243                Some("1.0".to_string()),
244            )
245            .await;
246
247        assert!(result.is_ok());
248        let response = result.unwrap();
249        assert_eq!(response.consent_type, "privacy_policy");
250        assert_eq!(response.policy_version, "1.0");
251        assert!(response.message.contains("privacy_policy"));
252    }
253
254    #[tokio::test]
255    async fn test_record_terms_consent() {
256        let uc = make_use_cases();
257        let result = uc
258            .record_consent(Uuid::new_v4(), Uuid::new_v4(), "terms", None, None, None)
259            .await;
260
261        assert!(result.is_ok());
262        assert_eq!(result.unwrap().consent_type, "terms");
263    }
264
265    #[tokio::test]
266    async fn test_record_invalid_consent_type() {
267        let uc = make_use_cases();
268        let result = uc
269            .record_consent(Uuid::new_v4(), Uuid::new_v4(), "invalid", None, None, None)
270            .await;
271
272        assert!(result.is_err());
273        assert!(result.unwrap_err().contains("Invalid consent type"));
274    }
275
276    #[tokio::test]
277    async fn test_get_consent_status_empty() {
278        let uc = make_use_cases();
279        let result = uc.get_consent_status(Uuid::new_v4()).await;
280
281        assert!(result.is_ok());
282        let status = result.unwrap();
283        assert!(!status.privacy_policy_accepted);
284        assert!(!status.terms_accepted);
285    }
286
287    #[tokio::test]
288    async fn test_get_consent_status_after_recording() {
289        let uc = make_use_cases();
290        let user_id = Uuid::new_v4();
291        let org_id = Uuid::new_v4();
292
293        uc.record_consent(user_id, org_id, "privacy_policy", None, None, None)
294            .await
295            .unwrap();
296
297        let status = uc.get_consent_status(user_id).await.unwrap();
298        assert!(status.privacy_policy_accepted);
299        assert!(!status.terms_accepted);
300    }
301
302    #[tokio::test]
303    async fn test_has_accepted() {
304        let uc = make_use_cases();
305        let user_id = Uuid::new_v4();
306        let org_id = Uuid::new_v4();
307
308        assert!(!uc.has_accepted(user_id, "terms").await.unwrap());
309
310        uc.record_consent(user_id, org_id, "terms", None, None, None)
311            .await
312            .unwrap();
313
314        assert!(uc.has_accepted(user_id, "terms").await.unwrap());
315    }
316}