use super::hackathon_dto::{ HackathonCreateRequestDto, HackathonDto, HackathonEventCreateRequestDto, HackathonTimelineCreateRequestDto, }; use super::hackathon_repository::HackathonRepository; use super::hackathon_audit_schema::{AuditAction, HackathonAuditLogSchema}; use super::hackathon_audit_repository::HackathonAuditRepository; use super::hackathon_validation::{validate_timeline_phases, validate_dates, validate_organizers, validate_prizes, MAX_EVENTS_PER_HACKATHON}; use crate::{AppState, ResponseSuccessDto, ErrorDto}; use axum::http::StatusCode; use anyhow::Result; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; use validator::Validate; /// Request DTO for atomic hackathon creation with timeline and events #[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] pub struct HackathonCompleteSetupRequestDto { #[validate(nested)] pub hackathon: HackathonCreateRequestDto, #[validate(length(min = 1, message = "At least one timeline phase is required"))] pub timelines: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub events: Option>, /// Actor ID for audit logging pub actor_id: String, #[serde(skip_serializing_if = "Option::is_none")] pub actor_email: Option, } /// Response DTO for complete hackathon setup #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] pub struct HackathonCompleteSetupResponseDto { pub hackathon: HackathonDto, pub timelines: Vec, pub events: Option>, pub message: String, } /// Service for atomic hackathon operations pub struct HackathonAtomicService; impl HackathonAtomicService { /// Create hackathon with timeline and events atomically /// This ensures all-or-nothing creation - if any step fails, nothing is created pub async fn create_hackathon_complete( payload: HackathonCompleteSetupRequestDto, state: &AppState, ) -> Result, ErrorDto> { // 1. Validate all inputs before any database operations if let Err((_, error_message)) = imphnen_utils::validator::validate_request(&payload) { return Err(ErrorDto { status: StatusCode::BAD_REQUEST.as_u16(), message: "Validation failed".to_string(), details: Some(serde_json::json!({ "validation_errors": error_message })), }); } // 2. Validate dates if let Err(e) = validate_dates( &payload.hackathon.start_date, &payload.hackathon.end_date, &payload.hackathon.registration_deadline, ) { return Err(ErrorDto { status: StatusCode::BAD_REQUEST.as_u16(), message: e.to_string(), details: None, }); } // 3. Validate organizers if let Err(e) = validate_organizers(&payload.hackathon.organizers) { return Err(ErrorDto { status: StatusCode::BAD_REQUEST.as_u16(), message: e.to_string(), details: None, }); } // 4. Validate prizes if provided if let Some(ref prizes) = payload.hackathon.prizes { let prize_schemas: Vec = prizes .iter() .map(|p| super::hackathon_schema::Prize { position: p.position, title: p.title.clone(), description: p.description.clone(), value: p.value.clone(), }) .collect(); if let Err(e) = validate_prizes(&prize_schemas) { return Err(ErrorDto { status: StatusCode::BAD_REQUEST.as_u16(), message: e.to_string(), details: None, }); } } // 5. Validate events count if provided if let Some(ref events) = payload.events { if events.len() > MAX_EVENTS_PER_HACKATHON { return Err(ErrorDto { status: StatusCode::BAD_REQUEST.as_u16(), message: format!( "Maximum {} events allowed per hackathon", MAX_EVENTS_PER_HACKATHON ), details: None, }); } } let repo = HackathonRepository::new(state); let audit_repo = HackathonAuditRepository::new(state); // 6. Create hackathon first let hackathon = match repo.create_hackathon(payload.hackathon.clone()).await { Ok(h) => h, Err(e) => { tracing::error!("Failed to create hackathon: {}", e); return Err(ErrorDto { status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(), message: "Failed to create hackathon".to_string(), details: Some(serde_json::json!({ "error": e.to_string() })), }); } }; let hackathon_id = hackathon.id.id.to_string(); // 7. Create timelines - if this fails, we should ideally rollback hackathon let mut created_timelines = Vec::new(); for timeline_dto in &payload.timelines { match repo.create_hackathon_timeline(hackathon_id.clone(), timeline_dto.clone()).await { Ok(timeline) => created_timelines.push(timeline), Err(e) => { tracing::error!("Failed to create timeline, attempting cleanup: {}", e); // Attempt to delete hackathon and created timelines let _ = Self::cleanup_failed_creation( &hackathon_id, &created_timelines.iter().map(|t| t.id.id.to_string()).collect::>(), &[], &repo, ) .await; return Err(ErrorDto { status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(), message: "Failed to create timeline, changes rolled back".to_string(), details: Some(serde_json::json!({ "error": e.to_string() })), }); } } } // 8. Validate timeline phases after all are created if let Err(e) = validate_timeline_phases(&hackathon, &created_timelines) { tracing::error!("Timeline validation failed, attempting cleanup: {}", e); let _ = Self::cleanup_failed_creation( &hackathon_id, &created_timelines.iter().map(|t| t.id.id.to_string()).collect::>(), &[], &repo, ) .await; return Err(ErrorDto { status: StatusCode::BAD_REQUEST.as_u16(), message: format!("Timeline validation failed: {}", e), details: None, }); } // 9. Create events if provided let mut created_events = Vec::new(); if let Some(ref events) = payload.events { for event_dto in events { match repo.create_hackathon_event(hackathon_id.clone(), event_dto.clone()).await { Ok(event) => created_events.push(event), Err(e) => { tracing::error!("Failed to create event, attempting cleanup: {}", e); let _ = Self::cleanup_failed_creation( &hackathon_id, &created_timelines.iter().map(|t| t.id.id.to_string()).collect::>(), &created_events.iter().map(|e| e.id.id.to_string()).collect::>(), &repo, ) .await; return Err(ErrorDto { status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(), message: "Failed to create event, changes rolled back".to_string(), details: Some(serde_json::json!({ "error": e.to_string() })), }); } } } } // 10. Log audit trail let audit_log = HackathonAuditLogSchema::new( Some(hackathon.id.clone()), AuditAction::HackathonCreated, payload.actor_id.clone(), "hackathon".to_string(), Some(hackathon_id.clone()), ) .with_changes(serde_json::to_value(&payload).unwrap_or_default()) .with_request_info(None, None, payload.actor_email.clone()); if let Err(e) = audit_repo.log(audit_log).await { tracing::error!("Failed to create audit log: {}", e); // Don't fail the request if audit logging fails } // 11. Return success response let response = HackathonCompleteSetupResponseDto { hackathon: super::hackathon_dto::HackathonDto::from(hackathon), timelines: created_timelines .into_iter() .map(super::hackathon_dto::HackathonTimelineDto::from) .collect(), events: if created_events.is_empty() { None } else { Some( created_events .into_iter() .map(super::hackathon_dto::HackathonEventDto::from) .collect(), ) }, message: "Hackathon created successfully with timeline and events".to_string(), }; Ok(ResponseSuccessDto { data: response }) } /// Cleanup failed creation by deleting created resources async fn cleanup_failed_creation( hackathon_id: &str, timeline_ids: &[String], event_ids: &[String], repo: &HackathonRepository<'_>, ) -> Result<()> { tracing::info!("Starting cleanup for failed hackathon creation"); // Delete events for event_id in event_ids { if let Err(e) = repo.delete_hackathon_event(event_id.to_string()).await { tracing::error!("Failed to cleanup event {}: {}", event_id, e); } } // Delete timelines for timeline_id in timeline_ids { if let Err(e) = repo.delete_hackathon_timeline(timeline_id.to_string()).await { tracing::error!("Failed to cleanup timeline {}: {}", timeline_id, e); } } // Delete hackathon if let Err(e) = repo.delete_hackathon(hackathon_id.to_string()).await { tracing::error!("Failed to cleanup hackathon {}: {}", hackathon_id, e); } tracing::info!("Cleanup completed"); Ok(()) } }