koprogo_api/domain/entities/
consent.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5/// Valid consent types matching database CHECK constraint
6const VALID_CONSENT_TYPES: [&str; 2] = ["privacy_policy", "terms"];
7
8/// ConsentRecord - GDPR Art. 7 / Art. 13-14 compliance
9///
10/// Tracks explicit user consent to privacy policy and terms of service.
11/// Each record is immutable (append-only) — new consent creates a new record.
12/// Audit trail includes IP address, user agent, and policy version.
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
14pub struct ConsentRecord {
15    pub id: Uuid,
16    pub user_id: Uuid,
17    pub organization_id: Uuid,
18    pub consent_type: String,
19    pub accepted_at: DateTime<Utc>,
20    pub ip_address: Option<String>,
21    pub user_agent: Option<String>,
22    pub policy_version: String,
23    pub created_at: DateTime<Utc>,
24    pub updated_at: DateTime<Utc>,
25}
26
27impl ConsentRecord {
28    /// Create a new consent record with validation
29    ///
30    /// # Arguments
31    /// - `user_id`: The user giving consent
32    /// - `organization_id`: The organization context
33    /// - `consent_type`: Must be "privacy_policy" or "terms"
34    /// - `ip_address`: Optional IP for audit trail
35    /// - `user_agent`: Optional browser user-agent for audit trail
36    /// - `policy_version`: Version of the policy being accepted (default "1.0")
37    pub fn new(
38        user_id: Uuid,
39        organization_id: Uuid,
40        consent_type: &str,
41        ip_address: Option<String>,
42        user_agent: Option<String>,
43        policy_version: Option<String>,
44    ) -> Result<Self, String> {
45        // Validate consent_type
46        if !VALID_CONSENT_TYPES.contains(&consent_type) {
47            return Err(format!(
48                "Invalid consent type '{}'. Must be one of: {}",
49                consent_type,
50                VALID_CONSENT_TYPES.join(", ")
51            ));
52        }
53
54        let version = policy_version.unwrap_or_else(|| "1.0".to_string());
55        if version.is_empty() {
56            return Err("Policy version cannot be empty".to_string());
57        }
58
59        let now = Utc::now();
60
61        Ok(Self {
62            id: Uuid::new_v4(),
63            user_id,
64            organization_id,
65            consent_type: consent_type.to_string(),
66            accepted_at: now,
67            ip_address,
68            user_agent,
69            policy_version: version,
70            created_at: now,
71            updated_at: now,
72        })
73    }
74
75    /// Check if this consent is for privacy policy
76    pub fn is_privacy_policy(&self) -> bool {
77        self.consent_type == "privacy_policy"
78    }
79
80    /// Check if this consent is for terms of service
81    pub fn is_terms(&self) -> bool {
82        self.consent_type == "terms"
83    }
84}
85
86/// Consent status summary for a user
87#[derive(Debug, Clone, Default, Serialize, Deserialize)]
88pub struct ConsentStatus {
89    pub privacy_policy_accepted: bool,
90    pub terms_accepted: bool,
91    pub privacy_policy_accepted_at: Option<DateTime<Utc>>,
92    pub terms_accepted_at: Option<DateTime<Utc>>,
93    pub privacy_policy_version: Option<String>,
94    pub terms_version: Option<String>,
95}
96
97
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn test_new_privacy_policy_consent() {
105        let user_id = Uuid::new_v4();
106        let org_id = Uuid::new_v4();
107        let record = ConsentRecord::new(
108            user_id,
109            org_id,
110            "privacy_policy",
111            Some("192.168.1.1".to_string()),
112            Some("Mozilla/5.0".to_string()),
113            Some("1.2".to_string()),
114        )
115        .unwrap();
116
117        assert_eq!(record.user_id, user_id);
118        assert_eq!(record.organization_id, org_id);
119        assert_eq!(record.consent_type, "privacy_policy");
120        assert_eq!(record.policy_version, "1.2");
121        assert!(record.is_privacy_policy());
122        assert!(!record.is_terms());
123        assert_eq!(record.ip_address.as_deref(), Some("192.168.1.1"));
124        assert_eq!(record.user_agent.as_deref(), Some("Mozilla/5.0"));
125    }
126
127    #[test]
128    fn test_new_terms_consent() {
129        let record =
130            ConsentRecord::new(Uuid::new_v4(), Uuid::new_v4(), "terms", None, None, None).unwrap();
131
132        assert_eq!(record.consent_type, "terms");
133        assert_eq!(record.policy_version, "1.0"); // default
134        assert!(record.is_terms());
135        assert!(!record.is_privacy_policy());
136    }
137
138    #[test]
139    fn test_invalid_consent_type() {
140        let result = ConsentRecord::new(
141            Uuid::new_v4(),
142            Uuid::new_v4(),
143            "invalid_type",
144            None,
145            None,
146            None,
147        );
148        assert!(result.is_err());
149        assert!(result
150            .unwrap_err()
151            .contains("Invalid consent type 'invalid_type'"));
152    }
153
154    #[test]
155    fn test_empty_policy_version() {
156        let result = ConsentRecord::new(
157            Uuid::new_v4(),
158            Uuid::new_v4(),
159            "privacy_policy",
160            None,
161            None,
162            Some("".to_string()),
163        );
164        assert!(result.is_err());
165        assert!(result
166            .unwrap_err()
167            .contains("Policy version cannot be empty"));
168    }
169
170    #[test]
171    fn test_consent_generates_unique_ids() {
172        let r1 =
173            ConsentRecord::new(Uuid::new_v4(), Uuid::new_v4(), "terms", None, None, None).unwrap();
174        let r2 =
175            ConsentRecord::new(Uuid::new_v4(), Uuid::new_v4(), "terms", None, None, None).unwrap();
176        assert_ne!(r1.id, r2.id);
177    }
178
179    #[test]
180    fn test_consent_status_default() {
181        let status = ConsentStatus::default();
182        assert!(!status.privacy_policy_accepted);
183        assert!(!status.terms_accepted);
184        assert!(status.privacy_policy_accepted_at.is_none());
185        assert!(status.terms_accepted_at.is_none());
186    }
187}