Skip to main content

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