koprogo_api/domain/entities/
gdpr_restriction.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5/// GDPR Article 18 - Right to Restriction of Processing
6///
7/// Represents a user's request to temporarily restrict processing of their personal data.
8/// During restriction, data can be stored but not processed (except with user consent or for legal claims).
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
10pub struct GdprRestrictionRequest {
11    pub id: Uuid,
12    pub user_id: Uuid,
13    pub organization_id: Option<Uuid>,
14    pub requested_at: DateTime<Utc>,
15    pub status: RestrictionStatus,
16    pub reason: RestrictionReason,
17    pub justification: Option<String>,
18    pub effective_from: Option<DateTime<Utc>>,
19    pub effective_until: Option<DateTime<Utc>>,
20    pub processed_at: Option<DateTime<Utc>>,
21    pub processed_by: Option<Uuid>,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
25pub enum RestrictionStatus {
26    Pending,
27    Active,  // Restriction is in effect
28    Lifted,  // Restriction was removed
29    Expired, // Restriction period ended
30    Rejected,
31}
32
33/// Grounds for restriction under Article 18(1)
34#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
35pub enum RestrictionReason {
36    /// (a) accuracy of the data is contested by the data subject
37    AccuracyContested,
38    /// (b) processing is unlawful and data subject opposes erasure
39    UnlawfulProcessing,
40    /// (c) controller no longer needs data but data subject needs it for legal claims
41    LegalClaims,
42    /// (d) data subject has objected to processing pending verification
43    ObjectionPending,
44}
45
46impl GdprRestrictionRequest {
47    /// Create a new restriction request
48    pub fn new(
49        user_id: Uuid,
50        organization_id: Option<Uuid>,
51        reason: RestrictionReason,
52        justification: Option<String>,
53    ) -> Self {
54        Self {
55            id: Uuid::new_v4(),
56            user_id,
57            organization_id,
58            requested_at: Utc::now(),
59            status: RestrictionStatus::Pending,
60            reason,
61            justification,
62            effective_from: None,
63            effective_until: None,
64            processed_at: None,
65            processed_by: None,
66        }
67    }
68
69    /// Activate the restriction
70    pub fn activate(&mut self, admin_id: Uuid, duration_days: Option<u32>) {
71        let now = Utc::now();
72        self.status = RestrictionStatus::Active;
73        self.effective_from = Some(now);
74        self.effective_until = duration_days.map(|days| now + chrono::Duration::days(days as i64));
75        self.processed_at = Some(now);
76        self.processed_by = Some(admin_id);
77    }
78
79    /// Lift the restriction
80    pub fn lift(&mut self, admin_id: Uuid) {
81        self.status = RestrictionStatus::Lifted;
82        self.effective_until = Some(Utc::now());
83        self.processed_at = Some(Utc::now());
84        self.processed_by = Some(admin_id);
85    }
86
87    /// Reject the restriction request
88    pub fn reject(&mut self, admin_id: Uuid) {
89        self.status = RestrictionStatus::Rejected;
90        self.processed_at = Some(Utc::now());
91        self.processed_by = Some(admin_id);
92    }
93
94    /// Check if restriction is currently active
95    pub fn is_active(&self) -> bool {
96        if self.status != RestrictionStatus::Active {
97            return false;
98        }
99
100        let now = Utc::now();
101
102        // Check if started
103        if let Some(from) = self.effective_from {
104            if now < from {
105                return false;
106            }
107        }
108
109        // Check if expired
110        if let Some(until) = self.effective_until {
111            if now > until {
112                return false;
113            }
114        }
115
116        true
117    }
118
119    /// Check if restriction is pending
120    pub fn is_pending(&self) -> bool {
121        matches!(self.status, RestrictionStatus::Pending)
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn test_create_restriction_request() {
131        let user_id = Uuid::new_v4();
132        let org_id = Uuid::new_v4();
133
134        let request = GdprRestrictionRequest::new(
135            user_id,
136            Some(org_id),
137            RestrictionReason::AccuracyContested,
138            Some("My email address is incorrect".to_string()),
139        );
140
141        assert_eq!(request.user_id, user_id);
142        assert_eq!(request.organization_id, Some(org_id));
143        assert!(request.is_pending());
144        assert_eq!(request.reason, RestrictionReason::AccuracyContested);
145    }
146
147    #[test]
148    fn test_activate_restriction_without_duration() {
149        let user_id = Uuid::new_v4();
150        let admin_id = Uuid::new_v4();
151
152        let mut request =
153            GdprRestrictionRequest::new(user_id, None, RestrictionReason::ObjectionPending, None);
154
155        request.activate(admin_id, None);
156
157        assert_eq!(request.status, RestrictionStatus::Active);
158        assert!(request.effective_from.is_some());
159        assert!(request.effective_until.is_none());
160        assert!(request.is_active());
161    }
162
163    #[test]
164    fn test_activate_restriction_with_duration() {
165        let user_id = Uuid::new_v4();
166        let admin_id = Uuid::new_v4();
167
168        let mut request =
169            GdprRestrictionRequest::new(user_id, None, RestrictionReason::AccuracyContested, None);
170
171        request.activate(admin_id, Some(30)); // 30 days
172
173        assert_eq!(request.status, RestrictionStatus::Active);
174        assert!(request.effective_from.is_some());
175        assert!(request.effective_until.is_some());
176        assert!(request.is_active());
177
178        // Check duration is approximately 30 days
179        let from = request.effective_from.unwrap();
180        let until = request.effective_until.unwrap();
181        let duration = until - from;
182        assert!((duration.num_days() - 30).abs() < 1);
183    }
184
185    #[test]
186    fn test_lift_restriction() {
187        let user_id = Uuid::new_v4();
188        let admin_id = Uuid::new_v4();
189
190        let mut request =
191            GdprRestrictionRequest::new(user_id, None, RestrictionReason::UnlawfulProcessing, None);
192
193        request.activate(admin_id, None);
194        assert!(request.is_active());
195
196        request.lift(admin_id);
197        assert_eq!(request.status, RestrictionStatus::Lifted);
198        assert!(!request.is_active());
199    }
200
201    #[test]
202    fn test_reject_restriction() {
203        let user_id = Uuid::new_v4();
204        let admin_id = Uuid::new_v4();
205
206        let mut request =
207            GdprRestrictionRequest::new(user_id, None, RestrictionReason::LegalClaims, None);
208
209        request.reject(admin_id);
210
211        assert_eq!(request.status, RestrictionStatus::Rejected);
212        assert!(!request.is_active());
213        assert!(!request.is_pending());
214    }
215}