koprogo_api/infrastructure/web/handlers/
gdpr_handlers.rs

1use crate::application::dto::{
2    GdprActionResponse, GdprMarketingPreferenceRequest, GdprRectifyRequest,
3    GdprRestrictProcessingRequest,
4};
5use crate::infrastructure::audit::{AuditEventType, AuditLogEntry};
6use crate::infrastructure::web::{AppState, AuthenticatedUser};
7use actix_web::{delete, get, put, web, HttpRequest, HttpResponse, Responder};
8use chrono::Utc;
9use tokio::spawn;
10
11/// Extract client IP address from request
12fn extract_ip_address(req: &HttpRequest) -> Option<String> {
13    // Try X-Forwarded-For first (for proxy/load balancer scenarios)
14    req.headers()
15        .get("X-Forwarded-For")
16        .and_then(|h| h.to_str().ok())
17        .map(|s| s.split(',').next().unwrap_or("").trim().to_string())
18        .filter(|s| !s.is_empty())
19        .or_else(|| {
20            // Try X-Real-IP header
21            req.headers()
22                .get("X-Real-IP")
23                .and_then(|h| h.to_str().ok())
24                .map(|s| s.to_string())
25        })
26        .or_else(|| {
27            // Fall back to peer address
28            req.peer_addr().map(|addr| addr.ip().to_string())
29        })
30}
31
32/// Extract user agent from request
33fn extract_user_agent(req: &HttpRequest) -> Option<String> {
34    req.headers()
35        .get("User-Agent")
36        .and_then(|h| h.to_str().ok())
37        .map(|s| s.to_string())
38}
39
40/// GET /api/v1/gdpr/export
41/// Export all personal data for the authenticated user (GDPR Article 15 - Right to Access)
42///
43/// # Returns
44/// * `200 OK` - JSON with complete user data export
45/// * `401 Unauthorized` - Missing or invalid authentication
46/// * `403 Forbidden` - User attempting to export another user's data
47/// * `404 Not Found` - User not found
48/// * `500 Internal Server Error` - Database or processing error
49#[utoipa::path(
50    get,
51    path = "/gdpr/export",
52    tag = "GDPR",
53    summary = "Export user personal data (Article 15 - Right to Access)",
54    responses(
55        (status = 200, description = "User data exported"),
56        (status = 401, description = "Unauthorized"),
57        (status = 403, description = "Forbidden"),
58        (status = 404, description = "User not found"),
59        (status = 410, description = "User already anonymized"),
60        (status = 500, description = "Internal server error"),
61    ),
62    security(("bearer_auth" = []))
63)]
64#[get("/gdpr/export")]
65pub async fn export_user_data(
66    req: HttpRequest,
67    data: web::Data<AppState>,
68    auth: AuthenticatedUser,
69) -> impl Responder {
70    // Extract user_id from authenticated user
71    let user_id = auth.user_id;
72
73    // Extract client information for audit logging
74    let ip_address = extract_ip_address(&req);
75    let user_agent = extract_user_agent(&req);
76
77    // Determine organization scope based on role
78    // SuperAdmin can export across all organizations (organization_id = None)
79    // Regular users are scoped to their organization
80    let organization_id = if auth.role == "superadmin" {
81        None
82    } else {
83        auth.organization_id
84    };
85
86    // Call use case to export data
87    match data
88        .gdpr_use_cases
89        .export_user_data(user_id, user_id, organization_id)
90        .await
91    {
92        Ok(export_data) => {
93            // Extract user info for email notification
94            let user_email = export_data.user.email.clone();
95            let user_name = format!(
96                "{} {}",
97                export_data.user.first_name, export_data.user.last_name
98            );
99
100            // Audit log: successful GDPR data export (async with database persistence)
101            let audit_entry = AuditLogEntry::new(
102                AuditEventType::GdprDataExported,
103                Some(user_id),
104                organization_id,
105            )
106            .with_resource("User", user_id)
107            .with_client_info(ip_address, user_agent)
108            .with_metadata(serde_json::json!({
109                "total_items": export_data.total_items,
110                "export_date": export_data.export_date
111            }));
112
113            let audit_logger = data.audit_logger.clone();
114            spawn(async move {
115                audit_logger.log(&audit_entry).await;
116            });
117
118            // Send email notification (async)
119            let email_service = data.email_service.clone();
120            spawn(async move {
121                if let Err(e) = email_service
122                    .send_gdpr_export_notification(&user_email, &user_name, user_id)
123                    .await
124                {
125                    log::error!("Failed to send GDPR export email notification: {}", e);
126                }
127            });
128
129            HttpResponse::Ok().json(export_data)
130        }
131        Err(e) => {
132            // Audit log: failed GDPR data export (async with database persistence)
133            let audit_entry = AuditLogEntry::new(
134                AuditEventType::GdprDataExportFailed,
135                Some(user_id),
136                organization_id,
137            )
138            .with_resource("User", user_id)
139            .with_client_info(ip_address, user_agent)
140            .with_error(e.clone());
141
142            let audit_logger = data.audit_logger.clone();
143            spawn(async move {
144                audit_logger.log(&audit_entry).await;
145            });
146
147            if e.contains("not found") {
148                HttpResponse::NotFound().json(serde_json::json!({
149                    "error": e
150                }))
151            } else if e.contains("Unauthorized") {
152                HttpResponse::Forbidden().json(serde_json::json!({
153                    "error": e
154                }))
155            } else if e.contains("anonymized") {
156                HttpResponse::Gone().json(serde_json::json!({
157                    "error": e
158                }))
159            } else {
160                HttpResponse::InternalServerError().json(serde_json::json!({
161                    "error": format!("Failed to export user data: {}", e)
162                }))
163            }
164        }
165    }
166}
167
168/// DELETE /api/v1/gdpr/erase
169/// Erase user personal data by anonymization (GDPR Article 17 - Right to Erasure)
170///
171/// This endpoint anonymizes the user's account and all linked owner profiles.
172/// Data is not deleted entirely to preserve referential integrity and comply with
173/// legal retention requirements (e.g., financial records must be kept for 7 years).
174///
175/// # Returns
176/// * `200 OK` - JSON confirmation of successful anonymization
177/// * `401 Unauthorized` - Missing or invalid authentication
178/// * `403 Forbidden` - User attempting to erase another user's data
179/// * `409 Conflict` - Legal holds prevent erasure (e.g., unpaid expenses)
180/// * `410 Gone` - User already anonymized
181/// * `500 Internal Server Error` - Database or processing error
182#[utoipa::path(
183    delete,
184    path = "/gdpr/erase",
185    tag = "GDPR",
186    summary = "Erase user data by anonymization (Article 17 - Right to Erasure)",
187    responses(
188        (status = 200, description = "User data anonymized"),
189        (status = 401, description = "Unauthorized"),
190        (status = 403, description = "Forbidden"),
191        (status = 404, description = "User not found"),
192        (status = 409, description = "Legal holds prevent erasure"),
193        (status = 410, description = "User already anonymized"),
194        (status = 500, description = "Internal server error"),
195    ),
196    security(("bearer_auth" = []))
197)]
198#[delete("/gdpr/erase")]
199pub async fn erase_user_data(
200    req: HttpRequest,
201    data: web::Data<AppState>,
202    auth: AuthenticatedUser,
203) -> impl Responder {
204    // Extract user_id from authenticated user
205    let user_id = auth.user_id;
206
207    // Extract client information for audit logging
208    let ip_address = extract_ip_address(&req);
209    let user_agent = extract_user_agent(&req);
210
211    // Determine organization scope based on role
212    let organization_id = if auth.role == "superadmin" {
213        None
214    } else {
215        auth.organization_id
216    };
217
218    // Call use case to erase data
219    match data
220        .gdpr_use_cases
221        .erase_user_data(user_id, user_id, organization_id)
222        .await
223    {
224        Ok(erase_response) => {
225            // Extract user info for email notification
226            let user_email = erase_response.user_email.clone();
227            let user_name = format!(
228                "{} {}",
229                erase_response.user_first_name, erase_response.user_last_name
230            );
231            let owners_count = erase_response.owners_anonymized;
232
233            // Audit log: successful GDPR data erasure (async with database persistence)
234            let audit_entry = AuditLogEntry::new(
235                AuditEventType::GdprDataErased,
236                Some(user_id),
237                organization_id,
238            )
239            .with_resource("User", user_id)
240            .with_client_info(ip_address, user_agent)
241            .with_metadata(serde_json::json!({
242                "owners_anonymized": erase_response.owners_anonymized,
243                "anonymized_at": erase_response.anonymized_at
244            }));
245
246            let audit_logger = data.audit_logger.clone();
247            spawn(async move {
248                audit_logger.log(&audit_entry).await;
249            });
250
251            // Send email notification (async)
252            let email_service = data.email_service.clone();
253            spawn(async move {
254                if let Err(e) = email_service
255                    .send_gdpr_erasure_notification(&user_email, &user_name, owners_count)
256                    .await
257                {
258                    log::error!("Failed to send GDPR erasure email notification: {}", e);
259                }
260            });
261
262            HttpResponse::Ok().json(erase_response)
263        }
264        Err(e) => {
265            // Audit log: failed GDPR data erasure (async with database persistence)
266            let audit_entry = AuditLogEntry::new(
267                AuditEventType::GdprDataErasureFailed,
268                Some(user_id),
269                organization_id,
270            )
271            .with_resource("User", user_id)
272            .with_client_info(ip_address, user_agent)
273            .with_error(e.clone());
274
275            let audit_logger = data.audit_logger.clone();
276            spawn(async move {
277                audit_logger.log(&audit_entry).await;
278            });
279
280            if e.contains("Unauthorized") {
281                HttpResponse::Forbidden().json(serde_json::json!({
282                    "error": e
283                }))
284            } else if e.contains("already anonymized") {
285                HttpResponse::Gone().json(serde_json::json!({
286                    "error": e
287                }))
288            } else if e.contains("legal holds") {
289                HttpResponse::Conflict().json(serde_json::json!({
290                    "error": e,
291                    "message": "Cannot erase data due to legal obligations. Please resolve pending issues before requesting erasure."
292                }))
293            } else if e.contains("not found") {
294                HttpResponse::NotFound().json(serde_json::json!({
295                    "error": e
296                }))
297            } else {
298                HttpResponse::InternalServerError().json(serde_json::json!({
299                    "error": format!("Failed to erase user data: {}", e)
300                }))
301            }
302        }
303    }
304}
305
306/// GET /api/v1/gdpr/can-erase
307/// Check if user data can be erased (no legal holds)
308///
309/// # Returns
310/// * `200 OK` - JSON with erasure eligibility status
311/// * `401 Unauthorized` - Missing or invalid authentication
312/// * `500 Internal Server Error` - Database or processing error
313#[utoipa::path(
314    get,
315    path = "/gdpr/can-erase",
316    tag = "GDPR",
317    summary = "Check if user data can be erased (no legal holds)",
318    responses(
319        (status = 200, description = "Erasure eligibility status returned"),
320        (status = 401, description = "Unauthorized"),
321        (status = 500, description = "Internal server error"),
322    ),
323    security(("bearer_auth" = []))
324)]
325#[get("/gdpr/can-erase")]
326pub async fn can_erase_user(
327    req: HttpRequest,
328    data: web::Data<AppState>,
329    auth: AuthenticatedUser,
330) -> impl Responder {
331    let user_id = auth.user_id;
332
333    // Extract client information for audit logging
334    let ip_address = extract_ip_address(&req);
335    let user_agent = extract_user_agent(&req);
336
337    match data.gdpr_use_cases.can_erase_user(user_id).await {
338        Ok(can_erase) => {
339            // Audit log: erasure check requested (async with database persistence)
340            let audit_entry = AuditLogEntry::new(
341                AuditEventType::GdprErasureCheckRequested,
342                Some(user_id),
343                auth.organization_id,
344            )
345            .with_resource("User", user_id)
346            .with_client_info(ip_address, user_agent)
347            .with_metadata(serde_json::json!({
348                "can_erase": can_erase
349            }));
350
351            let audit_logger = data.audit_logger.clone();
352            spawn(async move {
353                audit_logger.log(&audit_entry).await;
354            });
355
356            HttpResponse::Ok().json(serde_json::json!({
357                "can_erase": can_erase,
358                "user_id": user_id.to_string()
359            }))
360        }
361        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
362            "error": format!("Failed to check erasure eligibility: {}", e)
363        })),
364    }
365}
366
367/// PUT /api/v1/gdpr/rectify
368/// Rectify user personal data (GDPR Article 16 - Right to Rectification)
369///
370/// Allows users to correct inaccurate or incomplete personal data.
371///
372/// # Request Body
373/// ```json
374/// {
375///   "email": "new@example.com",        // Optional
376///   "first_name": "Jane",              // Optional
377///   "last_name": "Doe"                 // Optional
378/// }
379/// ```
380///
381/// # Returns
382/// * `200 OK` - Data successfully rectified
383/// * `400 Bad Request` - Validation error (e.g., invalid email)
384/// * `401 Unauthorized` - Missing or invalid authentication
385/// * `403 Forbidden` - User attempting to rectify another user's data
386/// * `404 Not Found` - User not found
387/// * `500 Internal Server Error` - Database or processing error
388#[utoipa::path(
389    put,
390    path = "/gdpr/rectify",
391    tag = "GDPR",
392    summary = "Rectify user personal data (Article 16 - Right to Rectification)",
393    request_body = GdprRectifyRequest,
394    responses(
395        (status = 200, description = "Data successfully rectified"),
396        (status = 400, description = "Validation error"),
397        (status = 401, description = "Unauthorized"),
398        (status = 403, description = "Forbidden"),
399        (status = 404, description = "User not found"),
400        (status = 500, description = "Internal server error"),
401    ),
402    security(("bearer_auth" = []))
403)]
404#[put("/gdpr/rectify")]
405pub async fn rectify_user_data(
406    req: HttpRequest,
407    data: web::Data<AppState>,
408    auth: AuthenticatedUser,
409    request: web::Json<GdprRectifyRequest>,
410) -> impl Responder {
411    let user_id = auth.user_id;
412
413    // Extract client information for audit logging
414    let ip_address = extract_ip_address(&req);
415    let user_agent = extract_user_agent(&req);
416
417    // Call use case to rectify data
418    match data
419        .gdpr_use_cases
420        .rectify_user_data(
421            user_id,
422            user_id, // Users can only rectify their own data
423            request.email.clone(),
424            request.first_name.clone(),
425            request.last_name.clone(),
426        )
427        .await
428    {
429        Ok(_) => {
430            // Audit log: successful data rectification (async with database persistence)
431            let audit_entry = AuditLogEntry::new(
432                AuditEventType::GdprDataRectified,
433                Some(user_id),
434                auth.organization_id,
435            )
436            .with_resource("User", user_id)
437            .with_client_info(ip_address, user_agent)
438            .with_metadata(serde_json::json!({
439                "fields_updated": {
440                    "email": request.email.is_some(),
441                    "first_name": request.first_name.is_some(),
442                    "last_name": request.last_name.is_some()
443                }
444            }));
445
446            let audit_logger = data.audit_logger.clone();
447            spawn(async move {
448                audit_logger.log(&audit_entry).await;
449            });
450
451            let response = GdprActionResponse {
452                success: true,
453                message: "Personal data successfully rectified".to_string(),
454                updated_at: Utc::now().to_rfc3339(),
455            };
456
457            HttpResponse::Ok().json(response)
458        }
459        Err(e) => {
460            // Audit log: failed data rectification
461            let audit_entry = AuditLogEntry::new(
462                AuditEventType::GdprDataRectificationFailed,
463                Some(user_id),
464                auth.organization_id,
465            )
466            .with_resource("User", user_id)
467            .with_client_info(ip_address, user_agent)
468            .with_error(e.clone());
469
470            let audit_logger = data.audit_logger.clone();
471            spawn(async move {
472                audit_logger.log(&audit_entry).await;
473            });
474
475            if e.contains("Unauthorized") {
476                HttpResponse::Forbidden().json(serde_json::json!({
477                    "error": e
478                }))
479            } else if e.contains("not found") {
480                HttpResponse::NotFound().json(serde_json::json!({
481                    "error": e
482                }))
483            } else if e.contains("Validation error")
484                || e.contains("Invalid email")
485                || e.contains("cannot be empty")
486                || e.contains("No fields provided")
487            {
488                HttpResponse::BadRequest().json(serde_json::json!({
489                    "error": e
490                }))
491            } else {
492                HttpResponse::InternalServerError().json(serde_json::json!({
493                    "error": format!("Failed to rectify user data: {}", e)
494                }))
495            }
496        }
497    }
498}
499
500/// PUT /api/v1/gdpr/restrict-processing
501/// Restrict data processing (GDPR Article 18 - Right to Restriction of Processing)
502///
503/// Allows users to request temporary limitation of data processing.
504/// When processing is restricted:
505/// - Data is stored but not processed for certain operations
506/// - Marketing communications are blocked
507/// - Profiling/analytics are disabled
508///
509/// # Returns
510/// * `200 OK` - Processing restriction applied
511/// * `400 Bad Request` - Processing already restricted
512/// * `401 Unauthorized` - Missing or invalid authentication
513/// * `403 Forbidden` - User attempting to restrict another user's processing
514/// * `404 Not Found` - User not found
515/// * `500 Internal Server Error` - Database or processing error
516#[utoipa::path(
517    put,
518    path = "/gdpr/restrict-processing",
519    tag = "GDPR",
520    summary = "Restrict data processing (Article 18 - Right to Restriction)",
521    request_body = GdprRestrictProcessingRequest,
522    responses(
523        (status = 200, description = "Processing restriction applied"),
524        (status = 400, description = "Processing already restricted"),
525        (status = 401, description = "Unauthorized"),
526        (status = 403, description = "Forbidden"),
527        (status = 404, description = "User not found"),
528        (status = 500, description = "Internal server error"),
529    ),
530    security(("bearer_auth" = []))
531)]
532#[put("/gdpr/restrict-processing")]
533pub async fn restrict_user_processing(
534    req: HttpRequest,
535    data: web::Data<AppState>,
536    auth: AuthenticatedUser,
537    _request: web::Json<GdprRestrictProcessingRequest>,
538) -> impl Responder {
539    let user_id = auth.user_id;
540
541    // Extract client information for audit logging
542    let ip_address = extract_ip_address(&req);
543    let user_agent = extract_user_agent(&req);
544
545    // Call use case to restrict processing
546    match data
547        .gdpr_use_cases
548        .restrict_user_processing(user_id, user_id)
549        .await
550    {
551        Ok(_) => {
552            // Audit log: successful processing restriction (async with database persistence)
553            let audit_entry = AuditLogEntry::new(
554                AuditEventType::GdprProcessingRestricted,
555                Some(user_id),
556                auth.organization_id,
557            )
558            .with_resource("User", user_id)
559            .with_client_info(ip_address, user_agent);
560
561            let audit_logger = data.audit_logger.clone();
562            spawn(async move {
563                audit_logger.log(&audit_entry).await;
564            });
565
566            let response = GdprActionResponse {
567                success: true,
568                message: "Data processing successfully restricted. Your data will be stored but not processed for certain operations.".to_string(),
569                updated_at: Utc::now().to_rfc3339(),
570            };
571
572            HttpResponse::Ok().json(response)
573        }
574        Err(e) => {
575            // Audit log: failed processing restriction
576            let audit_entry = AuditLogEntry::new(
577                AuditEventType::GdprProcessingRestrictionFailed,
578                Some(user_id),
579                auth.organization_id,
580            )
581            .with_resource("User", user_id)
582            .with_client_info(ip_address, user_agent)
583            .with_error(e.clone());
584
585            let audit_logger = data.audit_logger.clone();
586            spawn(async move {
587                audit_logger.log(&audit_entry).await;
588            });
589
590            if e.contains("Unauthorized") {
591                HttpResponse::Forbidden().json(serde_json::json!({
592                    "error": e
593                }))
594            } else if e.contains("not found") {
595                HttpResponse::NotFound().json(serde_json::json!({
596                    "error": e
597                }))
598            } else if e.contains("already restricted") {
599                HttpResponse::BadRequest().json(serde_json::json!({
600                    "error": e
601                }))
602            } else {
603                HttpResponse::InternalServerError().json(serde_json::json!({
604                    "error": format!("Failed to restrict processing: {}", e)
605                }))
606            }
607        }
608    }
609}
610
611/// PUT /api/v1/gdpr/marketing-preference
612/// Set marketing opt-out preference (GDPR Article 21 - Right to Object)
613///
614/// Allows users to object to marketing communications and profiling.
615///
616/// # Request Body
617/// ```json
618/// {
619///   "opt_out": true  // true to opt out, false to opt back in
620/// }
621/// ```
622///
623/// # Returns
624/// * `200 OK` - Marketing preference updated
625/// * `401 Unauthorized` - Missing or invalid authentication
626/// * `403 Forbidden` - User attempting to change another user's preferences
627/// * `404 Not Found` - User not found
628/// * `500 Internal Server Error` - Database or processing error
629#[utoipa::path(
630    put,
631    path = "/gdpr/marketing-preference",
632    tag = "GDPR",
633    summary = "Set marketing opt-out preference (Article 21 - Right to Object)",
634    request_body = GdprMarketingPreferenceRequest,
635    responses(
636        (status = 200, description = "Marketing preference updated"),
637        (status = 401, description = "Unauthorized"),
638        (status = 403, description = "Forbidden"),
639        (status = 404, description = "User not found"),
640        (status = 500, description = "Internal server error"),
641    ),
642    security(("bearer_auth" = []))
643)]
644#[put("/gdpr/marketing-preference")]
645pub async fn set_marketing_preference(
646    req: HttpRequest,
647    data: web::Data<AppState>,
648    auth: AuthenticatedUser,
649    request: web::Json<GdprMarketingPreferenceRequest>,
650) -> impl Responder {
651    let user_id = auth.user_id;
652
653    // Extract client information for audit logging
654    let ip_address = extract_ip_address(&req);
655    let user_agent = extract_user_agent(&req);
656
657    let opt_out = request.opt_out;
658
659    // Call use case to set marketing preference
660    match data
661        .gdpr_use_cases
662        .set_marketing_preference(user_id, user_id, opt_out)
663        .await
664    {
665        Ok(_) => {
666            // Audit log: marketing preference change (async with database persistence)
667            let event_type = if opt_out {
668                AuditEventType::GdprMarketingOptOut
669            } else {
670                AuditEventType::GdprMarketingOptIn
671            };
672
673            let audit_entry = AuditLogEntry::new(event_type, Some(user_id), auth.organization_id)
674                .with_resource("User", user_id)
675                .with_client_info(ip_address, user_agent)
676                .with_metadata(serde_json::json!({
677                    "opt_out": opt_out
678                }));
679
680            let audit_logger = data.audit_logger.clone();
681            spawn(async move {
682                audit_logger.log(&audit_entry).await;
683            });
684
685            let message = if opt_out {
686                "You have successfully opted out of marketing communications. You will no longer receive promotional emails or offers."
687            } else {
688                "You have successfully opted back in to marketing communications. You will receive promotional emails and offers."
689            };
690
691            let response = GdprActionResponse {
692                success: true,
693                message: message.to_string(),
694                updated_at: Utc::now().to_rfc3339(),
695            };
696
697            HttpResponse::Ok().json(response)
698        }
699        Err(e) => {
700            // Audit log: failed marketing preference change
701            let audit_entry = AuditLogEntry::new(
702                AuditEventType::GdprMarketingPreferenceChangeFailed,
703                Some(user_id),
704                auth.organization_id,
705            )
706            .with_resource("User", user_id)
707            .with_client_info(ip_address, user_agent)
708            .with_error(e.clone());
709
710            let audit_logger = data.audit_logger.clone();
711            spawn(async move {
712                audit_logger.log(&audit_entry).await;
713            });
714
715            if e.contains("Unauthorized") {
716                HttpResponse::Forbidden().json(serde_json::json!({
717                    "error": e
718                }))
719            } else if e.contains("not found") {
720                HttpResponse::NotFound().json(serde_json::json!({
721                    "error": e
722                }))
723            } else {
724                HttpResponse::InternalServerError().json(serde_json::json!({
725                    "error": format!("Failed to set marketing preference: {}", e)
726                }))
727            }
728        }
729    }
730}
731
732#[cfg(test)]
733mod tests {
734    // Note: Full integration tests with actual AppState would require proper initialization
735    // of all use cases. These handler tests are covered by E2E tests in tests/e2e/
736
737    #[test]
738    fn test_handler_structure_export() {
739        // This test just verifies the handler function signature compiles
740        // Real testing happens in E2E tests with testcontainers
741    }
742
743    #[test]
744    fn test_handler_structure_erase() {
745        // This test just verifies the handler function signature compiles
746    }
747
748    #[test]
749    fn test_handler_structure_can_erase() {
750        // This test just verifies the handler function signature compiles
751    }
752}