Skip to main content

koprogo_api/infrastructure/web/handlers/
gdpr_handlers.rs

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