koprogo_api/application/dto/
gdpr_dto.rs

1use crate::domain::entities::gdpr_export::{
2    DocumentData, ExpenseData, GdprExport, MeetingData, OwnerData, UnitOwnershipData, UserData,
3};
4use serde::{Deserialize, Serialize};
5
6/// Response DTO for GDPR data export (Article 15)
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
8pub struct GdprExportResponseDto {
9    pub export_date: String, // RFC3339 format
10    pub user: UserDataDto,
11    pub owners: Vec<OwnerDataDto>,
12    pub units: Vec<UnitOwnershipDataDto>,
13    pub expenses: Vec<ExpenseDataDto>,
14    pub documents: Vec<DocumentDataDto>,
15    pub meetings: Vec<MeetingDataDto>,
16    pub total_items: usize,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
20pub struct UserDataDto {
21    pub id: String,
22    pub email: String,
23    pub first_name: String,
24    pub last_name: String,
25    pub organization_id: Option<String>,
26    pub is_active: bool,
27    pub is_anonymized: bool,
28    pub created_at: String,
29    pub updated_at: String,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
33pub struct OwnerDataDto {
34    pub id: String,
35    pub organization_id: String,
36    pub first_name: String,
37    pub last_name: String,
38    pub email: Option<String>,
39    pub phone: Option<String>,
40    pub address: Option<String>,
41    pub city: Option<String>,
42    pub postal_code: Option<String>,
43    pub country: Option<String>,
44    pub is_anonymized: bool,
45    pub created_at: String,
46    pub updated_at: String,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
50pub struct UnitOwnershipDataDto {
51    pub building_name: String,
52    pub building_address: String,
53    pub unit_number: String,
54    pub floor: Option<i32>,
55    pub ownership_percentage: f64,
56    pub start_date: String,
57    pub end_date: Option<String>,
58    pub is_primary_contact: bool,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
62pub struct ExpenseDataDto {
63    pub description: String,
64    pub amount: f64,
65    pub due_date: String,
66    pub paid: bool,
67    pub building_name: String,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
71pub struct DocumentDataDto {
72    pub title: String,
73    pub document_type: String,
74    pub uploaded_at: String,
75    pub building_name: Option<String>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
79pub struct MeetingDataDto {
80    pub title: String,
81    pub meeting_date: String,
82    pub agenda: Option<String>,
83    pub building_name: String,
84}
85
86impl From<GdprExport> for GdprExportResponseDto {
87    fn from(export: GdprExport) -> Self {
88        let total_items = export.total_items();
89        Self {
90            export_date: export.export_date.to_rfc3339(),
91            user: UserDataDto::from(export.user_data),
92            owners: export
93                .owner_profiles
94                .into_iter()
95                .map(OwnerDataDto::from)
96                .collect(),
97            units: export
98                .related_data
99                .units
100                .into_iter()
101                .map(UnitOwnershipDataDto::from)
102                .collect(),
103            expenses: export
104                .related_data
105                .expenses
106                .into_iter()
107                .map(ExpenseDataDto::from)
108                .collect(),
109            documents: export
110                .related_data
111                .documents
112                .into_iter()
113                .map(DocumentDataDto::from)
114                .collect(),
115            meetings: export
116                .related_data
117                .meetings
118                .into_iter()
119                .map(MeetingDataDto::from)
120                .collect(),
121            total_items,
122        }
123    }
124}
125
126impl From<UserData> for UserDataDto {
127    fn from(user: UserData) -> Self {
128        Self {
129            id: user.id.to_string(),
130            email: user.email,
131            first_name: user.first_name,
132            last_name: user.last_name,
133            organization_id: user.organization_id.map(|id| id.to_string()),
134            is_active: user.is_active,
135            is_anonymized: user.is_anonymized,
136            created_at: user.created_at.to_rfc3339(),
137            updated_at: user.updated_at.to_rfc3339(),
138        }
139    }
140}
141
142impl From<OwnerData> for OwnerDataDto {
143    fn from(owner: OwnerData) -> Self {
144        Self {
145            id: owner.id.to_string(),
146            organization_id: owner
147                .organization_id
148                .map(|id| id.to_string())
149                .unwrap_or_else(|| "none".to_string()),
150            first_name: owner.first_name,
151            last_name: owner.last_name,
152            email: owner.email,
153            phone: owner.phone,
154            address: owner.address,
155            city: owner.city,
156            postal_code: owner.postal_code,
157            country: owner.country,
158            is_anonymized: owner.is_anonymized,
159            created_at: owner.created_at.to_rfc3339(),
160            updated_at: owner.updated_at.to_rfc3339(),
161        }
162    }
163}
164
165impl From<UnitOwnershipData> for UnitOwnershipDataDto {
166    fn from(unit: UnitOwnershipData) -> Self {
167        Self {
168            building_name: unit.building_name,
169            building_address: unit.building_address,
170            unit_number: unit.unit_number,
171            floor: unit.floor,
172            ownership_percentage: unit.ownership_percentage,
173            start_date: unit.start_date.to_rfc3339(),
174            end_date: unit.end_date.map(|d| d.to_rfc3339()),
175            is_primary_contact: unit.is_primary_contact,
176        }
177    }
178}
179
180impl From<ExpenseData> for ExpenseDataDto {
181    fn from(expense: ExpenseData) -> Self {
182        Self {
183            description: expense.description,
184            amount: expense.amount,
185            due_date: expense.due_date.to_rfc3339(),
186            paid: expense.paid,
187            building_name: expense.building_name,
188        }
189    }
190}
191
192impl From<DocumentData> for DocumentDataDto {
193    fn from(document: DocumentData) -> Self {
194        Self {
195            title: document.title,
196            document_type: document.document_type,
197            uploaded_at: document.uploaded_at.to_rfc3339(),
198            building_name: document.building_name,
199        }
200    }
201}
202
203impl From<MeetingData> for MeetingDataDto {
204    fn from(meeting: MeetingData) -> Self {
205        Self {
206            title: meeting.title,
207            meeting_date: meeting.meeting_date.to_rfc3339(),
208            agenda: meeting.agenda,
209            building_name: meeting.building_name,
210        }
211    }
212}
213
214/// Request DTO for GDPR data erasure (Article 17)
215#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct GdprEraseRequestDto {
217    /// Optional confirmation token for security
218    pub confirmation: Option<String>,
219}
220
221/// Response DTO for GDPR data erasure
222#[derive(Debug, Clone, Serialize, Deserialize)]
223pub struct GdprEraseResponseDto {
224    pub success: bool,
225    pub message: String,
226    pub anonymized_at: String,
227    pub user_id: String,
228    pub user_email: String,
229    pub user_first_name: String,
230    pub user_last_name: String,
231    pub owners_anonymized: usize,
232}
233
234// GDPR Article 16: Right to Rectification
235#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct GdprRectifyRequest {
237    pub email: Option<String>,
238    pub first_name: Option<String>,
239    pub last_name: Option<String>,
240}
241
242// GDPR Article 18: Right to Restriction of Processing
243#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct GdprRestrictProcessingRequest {
245    // No body needed - action is the request itself
246}
247
248// GDPR Article 21: Right to Object (Marketing opt-out)
249#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct GdprMarketingPreferenceRequest {
251    pub opt_out: bool,
252}
253
254// Generic success response for GDPR actions
255#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct GdprActionResponse {
257    pub success: bool,
258    pub message: String,
259    pub updated_at: String,
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265    use chrono::Utc;
266    use uuid::Uuid;
267
268    fn create_test_gdpr_export() -> GdprExport {
269        let user_data = UserData {
270            id: Uuid::new_v4(),
271            email: "test@example.com".to_string(),
272            first_name: "John".to_string(),
273            last_name: "Doe".to_string(),
274            organization_id: Some(Uuid::new_v4()),
275            is_active: true,
276            is_anonymized: false,
277            created_at: Utc::now(),
278            updated_at: Utc::now(),
279        };
280
281        GdprExport::new(user_data)
282    }
283
284    #[test]
285    fn test_gdpr_export_response_dto_from_domain() {
286        let export = create_test_gdpr_export();
287        let user_email = export.user_data.email.clone();
288
289        let dto = GdprExportResponseDto::from(export);
290
291        assert_eq!(dto.user.email, user_email);
292        assert_eq!(dto.total_items, 1); // Only user
293        assert!(dto.export_date.contains('T')); // RFC3339 format
294    }
295
296    #[test]
297    fn test_user_data_dto_conversion() {
298        let user_data = UserData {
299            id: Uuid::new_v4(),
300            email: "test@example.com".to_string(),
301            first_name: "John".to_string(),
302            last_name: "Doe".to_string(),
303            organization_id: Some(Uuid::new_v4()),
304            is_active: true,
305            is_anonymized: false,
306            created_at: Utc::now(),
307            updated_at: Utc::now(),
308        };
309
310        let dto = UserDataDto::from(user_data.clone());
311
312        assert_eq!(dto.email, user_data.email);
313        assert_eq!(dto.first_name, user_data.first_name);
314        assert_eq!(dto.last_name, user_data.last_name);
315        assert!(!dto.is_anonymized);
316    }
317
318    #[test]
319    fn test_owner_data_dto_conversion() {
320        let owner_data = OwnerData {
321            id: Uuid::new_v4(),
322            organization_id: Some(Uuid::new_v4()),
323            first_name: "Jane".to_string(),
324            last_name: "Smith".to_string(),
325            email: Some("jane@example.com".to_string()),
326            phone: Some("+1234567890".to_string()),
327            address: Some("123 Main St".to_string()),
328            city: Some("Brussels".to_string()),
329            postal_code: Some("1000".to_string()),
330            country: Some("Belgium".to_string()),
331            is_anonymized: false,
332            created_at: Utc::now(),
333            updated_at: Utc::now(),
334        };
335
336        let dto = OwnerDataDto::from(owner_data.clone());
337
338        assert_eq!(dto.first_name, owner_data.first_name);
339        assert_eq!(dto.email, owner_data.email);
340        assert_eq!(dto.phone, owner_data.phone);
341    }
342
343    #[test]
344    fn test_json_serialization() {
345        let export = create_test_gdpr_export();
346        let dto = GdprExportResponseDto::from(export);
347
348        // Test JSON serialization
349        let json = serde_json::to_string(&dto).expect("Should serialize to JSON");
350        assert!(json.contains("export_date"));
351        assert!(json.contains("test@example.com"));
352        assert!(json.contains("total_items"));
353
354        // Test JSON deserialization
355        let deserialized: GdprExportResponseDto =
356            serde_json::from_str(&json).expect("Should deserialize from JSON");
357        assert_eq!(deserialized.user.email, dto.user.email);
358    }
359
360    #[test]
361    fn test_erase_request_dto() {
362        let request = GdprEraseRequestDto {
363            confirmation: Some("CONFIRM_DELETE".to_string()),
364        };
365
366        let json = serde_json::to_string(&request).expect("Should serialize");
367        assert!(json.contains("CONFIRM_DELETE"));
368    }
369
370    #[test]
371    fn test_erase_response_dto() {
372        let response = GdprEraseResponseDto {
373            success: true,
374            message: "Data successfully anonymized".to_string(),
375            anonymized_at: Utc::now().to_rfc3339(),
376            user_id: Uuid::new_v4().to_string(),
377            user_email: "test@example.com".to_string(),
378            user_first_name: "John".to_string(),
379            user_last_name: "Doe".to_string(),
380            owners_anonymized: 2,
381        };
382
383        let json = serde_json::to_string(&response).expect("Should serialize");
384        assert!(json.contains("success"));
385        assert!(json.contains("owners_anonymized"));
386        assert!(json.contains("test@example.com"));
387    }
388}