Files
imphnen-backend-service/imphnen-hackathon/src/v1/hackathon/hackathon_atomic_service.rs
T

278 lines
11 KiB
Rust

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<HackathonTimelineCreateRequestDto>,
#[serde(skip_serializing_if = "Option::is_none")]
pub events: Option<Vec<HackathonEventCreateRequestDto>>,
/// Actor ID for audit logging
pub actor_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub actor_email: Option<String>,
}
/// Response DTO for complete hackathon setup
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct HackathonCompleteSetupResponseDto {
pub hackathon: HackathonDto,
pub timelines: Vec<super::hackathon_dto::HackathonTimelineDto>,
pub events: Option<Vec<super::hackathon_dto::HackathonEventDto>>,
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<ResponseSuccessDto<HackathonCompleteSetupResponseDto>, 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<super::hackathon_schema::Prize> = 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::<Vec<_>>(),
&[],
&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::<Vec<_>>(),
&[],
&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::<Vec<_>>(),
&created_events.iter().map(|e| e.id.id.to_string()).collect::<Vec<_>>(),
&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(())
}
}