Add comprehensive tests for hackathon service functionality

- Implemented tests for creating, retrieving, updating, and deleting hackathons.
- Added validation tests for hackathon creation and updates.
- Included tests for hackathon events and timelines, ensuring proper handling of edge cases.
- Created tests for hackathon submissions, including validation and submission status updates.
- Organized tests into a dedicated module for better structure and maintainability.
This commit is contained in:
MythEclipse
2025-09-27 13:01:08 +07:00
parent ef1d63e893
commit e530eba60d
25 changed files with 5213 additions and 22 deletions
+41
View File
@@ -0,0 +1,41 @@
[package]
name = "imphnen-hackathon"
version = "0.1.0"
edition = "2024"
[dependencies]
imphnen-libs.workspace = true
imphnen-utils.workspace = true
imphnen-entities.workspace = true
async-trait.workspace = true
axum.workspace = true
serde.workspace = true
serde_json = { workspace = true }
oauth2 = { workspace = true }
reqwest = { workspace = true, features = ["json"] }
utoipa.workspace = true
lazy_static.workspace = true
regex.workspace = true
validator.workspace = true
axum-test.workspace = true
surrealdb.workspace = true
rand.workspace = true
tokio.workspace = true
chrono.workspace = true
anyhow.workspace = true
tower-http.workspace = true
utoipa-swagger-ui.workspace = true
strum.workspace = true
strum_macros.workspace = true
log.workspace = true
once_cell.workspace = true
tracing.workspace = true
uuid.workspace = true
axum-extra.workspace = true
[dev-dependencies]
dotenvy.workspace = true
tokio-test = { workspace = true }
mockall = { workspace = true }
http-body-util.workspace = true
+28
View File
@@ -0,0 +1,28 @@
pub mod v1;
// Re-export core entity types used across the hackathon system
pub use imphnen_entities::{
CountResult,
Error,
MessageResponseDto,
MetaRequestDto,
MetaResponseDto,
ResponseListSuccessDto,
ResponseSuccessDto,
};
// Error DTO for hackathon module
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, utoipa::ToSchema)]
pub struct ErrorDto {
pub status: u16,
pub message: String,
pub details: Option<serde_json::Value>,
}
// Explicitly import only what we need from libs and utils to avoid pollution
pub use imphnen_libs::{
AppState,
};
// Re-export public v1 API
pub use v1::hackathon::hackathon_controller::hackathon_routes;
@@ -0,0 +1,509 @@
use super::hackathon_dto::{
HackathonCreateRequestDto, HackathonDto, HackathonEventCreateRequestDto, HackathonEventDto,
HackathonEventUpdateRequestDto, HackathonSubmissionCreateRequestDto,
HackathonSubmissionDto, HackathonSubmissionUpdateRequestDto, HackathonTimelineCreateRequestDto,
HackathonTimelineDto, HackathonTimelineUpdateRequestDto, HackathonUpdateRequestDto,
};
use super::hackathon_service::{HackathonService, HackathonServiceTrait};
use crate::{AppState, ResponseSuccessDto, ErrorDto};
use imphnen_libs::{MetaRequestDto, ResponseListSuccessDto};
use axum::{
extract::{Extension, Path, Query},
http::StatusCode,
Json, Router,
response::IntoResponse,
routing::{delete, get, post, put},
};
// Hackathon routes
#[utoipa::path(
post,
path = "/v1/hackathons",
request_body = HackathonCreateRequestDto,
responses(
(status = 201, description = "Hackathon created successfully", body = ResponseSuccessDto<HackathonDto>),
(status = 400, description = "Bad request", body = ErrorDto),
(status = 500, description = "Internal server error", body = ErrorDto)
),
tag = "Hackathons"
)]
pub async fn create_hackathon(
Extension(state): Extension<AppState>,
Json(payload): Json<HackathonCreateRequestDto>,
) -> impl IntoResponse {
match HackathonService::create_hackathon(payload, &state).await {
Ok(response) => (axum::http::StatusCode::CREATED, Json(response)).into_response(),
Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(),
}
}
#[utoipa::path(
get,
path = "/v1/hackathons/{id}",
params(
("id" = String, Path, description = "Hackathon ID")
),
responses(
(status = 200, description = "Hackathon retrieved successfully", body = ResponseSuccessDto<HackathonDto>),
(status = 404, description = "Hackathon not found", body = ErrorDto),
(status = 500, description = "Internal server error", body = ErrorDto)
),
tag = "Hackathons"
)]
pub async fn get_hackathon(
Extension(state): Extension<AppState>,
Path(id): Path<String>,
) -> impl IntoResponse {
match HackathonService::get_hackathon(id, &state).await {
Ok(response) => (axum::http::StatusCode::OK, Json(response)).into_response(),
Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(),
}
}
#[utoipa::path(
get,
path = "/v1/hackathons",
params(
("page" = Option<i64>, Query, description = "Page number"),
("per_page" = Option<i64>, Query, description = "Items per page"),
("search" = Option<String>, Query, description = "Search keyword"),
("sort_by" = Option<String>, Query, description = "Sort by field"),
("order" = Option<String>, Query, description = "Order ASC or DESC"),
("filter" = Option<String>, Query, description = "Filter value"),
("filter_by" = Option<String>, Query, description = "Field to filter by"),
),
responses(
(status = 200, description = "Hackathons retrieved successfully", body = ResponseListSuccessDto<Vec<HackathonDto>>),
(status = 500, description = "Internal server error", body = ErrorDto)
),
tag = "Hackathons"
)]
pub async fn list_hackathons(
Extension(state): Extension<AppState>,
Query(meta): Query<MetaRequestDto>,
) -> impl IntoResponse {
match HackathonService::list_hackathons(meta, &state).await {
Ok(response) => (axum::http::StatusCode::OK, Json(response)).into_response(),
Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(),
}
}
#[utoipa::path(
put,
path = "/v1/hackathons/{id}",
params(
("id" = String, Path, description = "Hackathon ID")
),
request_body = HackathonUpdateRequestDto,
responses(
(status = 200, description = "Hackathon updated successfully", body = ResponseSuccessDto<HackathonDto>),
(status = 400, description = "Bad request", body = ErrorDto),
(status = 404, description = "Hackathon not found", body = ErrorDto),
(status = 500, description = "Internal server error", body = ErrorDto)
),
tag = "Hackathons"
)]
pub async fn update_hackathon(
Extension(state): Extension<AppState>,
Path(id): Path<String>,
Json(payload): Json<HackathonUpdateRequestDto>,
) -> impl IntoResponse {
match HackathonService::update_hackathon(id, payload, &state).await {
Ok(response) => (axum::http::StatusCode::OK, Json(response)).into_response(),
Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(),
}
}
#[utoipa::path(
delete,
path = "/v1/hackathons/{id}",
params(
("id" = String, Path, description = "Hackathon ID")
),
responses(
(status = 200, description = "Hackathon deleted successfully", body = ResponseSuccessDto<String>),
(status = 404, description = "Hackathon not found", body = ErrorDto),
(status = 500, description = "Internal server error", body = ErrorDto)
),
tag = "Hackathons"
)]
pub async fn delete_hackathon(
Extension(state): Extension<AppState>,
Path(id): Path<String>,
) -> impl IntoResponse {
match HackathonService::delete_hackathon(id, &state).await {
Ok(response) => (axum::http::StatusCode::OK, Json(response)).into_response(),
Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(),
}
}
// Hackathon Events routes
#[utoipa::path(
post,
path = "/v1/hackathons/{hackathon_id}/events",
params(
("hackathon_id" = String, Path, description = "Hackathon ID")
),
request_body = HackathonEventCreateRequestDto,
responses(
(status = 201, description = "Event created successfully", body = ResponseSuccessDto<HackathonEventDto>),
(status = 400, description = "Bad request", body = ErrorDto),
(status = 404, description = "Hackathon not found", body = ErrorDto),
(status = 500, description = "Internal server error", body = ErrorDto)
),
tag = "Hackathon Events"
)]
pub async fn create_hackathon_event(
Extension(state): Extension<AppState>,
Path(hackathon_id): Path<String>,
Json(payload): Json<HackathonEventCreateRequestDto>,
) -> impl IntoResponse {
match HackathonService::create_hackathon_event(hackathon_id, payload, &state).await {
Ok(response) => (axum::http::StatusCode::CREATED, Json(response)).into_response(),
Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(),
}
}
#[utoipa::path(
get,
path = "/v1/hackathons/{hackathon_id}/events",
params(
("hackathon_id" = String, Path, description = "Hackathon ID"),
("page" = Option<i64>, Query, description = "Page number"),
("per_page" = Option<i64>, Query, description = "Items per page"),
("search" = Option<String>, Query, description = "Search keyword"),
("sort_by" = Option<String>, Query, description = "Sort by field"),
("order" = Option<String>, Query, description = "Order ASC or DESC"),
("filter" = Option<String>, Query, description = "Filter value"),
("filter_by" = Option<String>, Query, description = "Field to filter by"),
),
responses(
(status = 200, description = "Events retrieved successfully", body = ResponseListSuccessDto<Vec<HackathonEventDto>>),
(status = 500, description = "Internal server error", body = ErrorDto)
),
tag = "Hackathon Events"
)]
pub async fn list_hackathon_events(
Extension(state): Extension<AppState>,
Path(hackathon_id): Path<String>,
Query(meta): Query<MetaRequestDto>,
) -> impl IntoResponse {
match HackathonService::list_hackathon_events(meta, hackathon_id, &state).await {
Ok(response) => (axum::http::StatusCode::OK, Json(response)).into_response(),
Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(),
}
}
#[utoipa::path(
put,
path = "/v1/hackathons/events/{id}",
params(
("id" = String, Path, description = "Event ID")
),
request_body = HackathonEventUpdateRequestDto,
responses(
(status = 200, description = "Event updated successfully", body = ResponseSuccessDto<HackathonEventDto>),
(status = 400, description = "Bad request", body = ErrorDto),
(status = 404, description = "Event not found", body = ErrorDto),
(status = 500, description = "Internal server error", body = ErrorDto)
),
tag = "Hackathon Events"
)]
pub async fn update_hackathon_event(
Extension(state): Extension<AppState>,
Path(id): Path<String>,
Json(payload): Json<HackathonEventUpdateRequestDto>,
) -> impl IntoResponse {
match HackathonService::update_hackathon_event(id, payload, &state).await {
Ok(response) => (axum::http::StatusCode::OK, Json(response)).into_response(),
Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(),
}
}
#[utoipa::path(
delete,
path = "/v1/hackathons/events/{id}",
params(
("id" = String, Path, description = "Event ID")
),
responses(
(status = 200, description = "Event deleted successfully", body = ResponseSuccessDto<String>),
(status = 404, description = "Event not found", body = ErrorDto),
(status = 500, description = "Internal server error", body = ErrorDto)
),
tag = "Hackathon Events"
)]
pub async fn delete_hackathon_event(
Extension(state): Extension<AppState>,
Path(id): Path<String>,
) -> impl IntoResponse {
match HackathonService::delete_hackathon_event(id, &state).await {
Ok(response) => (axum::http::StatusCode::OK, Json(response)).into_response(),
Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(),
}
}
// Hackathon Timeline routes
#[utoipa::path(
post,
path = "/v1/hackathons/{hackathon_id}/timeline",
params(
("hackathon_id" = String, Path, description = "Hackathon ID")
),
request_body = HackathonTimelineCreateRequestDto,
responses(
(status = 201, description = "Timeline created successfully", body = ResponseSuccessDto<HackathonTimelineDto>),
(status = 400, description = "Bad request", body = ErrorDto),
(status = 404, description = "Hackathon not found", body = ErrorDto),
(status = 500, description = "Internal server error", body = ErrorDto)
),
tag = "Hackathon Timeline"
)]
pub async fn create_hackathon_timeline(
Extension(state): Extension<AppState>,
Path(hackathon_id): Path<String>,
Json(payload): Json<HackathonTimelineCreateRequestDto>,
) -> impl IntoResponse {
match HackathonService::create_hackathon_timeline(hackathon_id, payload, &state).await {
Ok(response) => (axum::http::StatusCode::CREATED, Json(response)).into_response(),
Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(),
}
}
#[utoipa::path(
get,
path = "/v1/hackathons/{hackathon_id}/timeline",
params(
("hackathon_id" = String, Path, description = "Hackathon ID"),
("page" = Option<i64>, Query, description = "Page number"),
("per_page" = Option<i64>, Query, description = "Items per page"),
("search" = Option<String>, Query, description = "Search keyword"),
("sort_by" = Option<String>, Query, description = "Sort by field"),
("order" = Option<String>, Query, description = "Order ASC or DESC"),
("filter" = Option<String>, Query, description = "Filter value"),
("filter_by" = Option<String>, Query, description = "Field to filter by"),
),
responses(
(status = 200, description = "Timeline retrieved successfully", body = ResponseListSuccessDto<Vec<HackathonTimelineDto>>),
(status = 500, description = "Internal server error", body = ErrorDto)
),
tag = "Hackathon Timeline"
)]
pub async fn list_hackathon_timeline(
Extension(state): Extension<AppState>,
Path(hackathon_id): Path<String>,
Query(meta): Query<MetaRequestDto>,
) -> impl IntoResponse {
match HackathonService::list_hackathon_timeline(meta, hackathon_id, &state).await {
Ok(response) => (axum::http::StatusCode::OK, Json(response)).into_response(),
Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(),
}
}
#[utoipa::path(
put,
path = "/v1/hackathons/timeline/{id}",
params(
("id" = String, Path, description = "Timeline ID")
),
request_body = HackathonTimelineUpdateRequestDto,
responses(
(status = 200, description = "Timeline updated successfully", body = ResponseSuccessDto<HackathonTimelineDto>),
(status = 400, description = "Bad request", body = ErrorDto),
(status = 404, description = "Timeline not found", body = ErrorDto),
(status = 500, description = "Internal server error", body = ErrorDto)
),
tag = "Hackathon Timeline"
)]
pub async fn update_hackathon_timeline(
Extension(state): Extension<AppState>,
Path(id): Path<String>,
Json(payload): Json<HackathonTimelineUpdateRequestDto>,
) -> impl IntoResponse {
match HackathonService::update_hackathon_timeline(id, payload, &state).await {
Ok(response) => (axum::http::StatusCode::OK, Json(response)).into_response(),
Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(),
}
}
#[utoipa::path(
delete,
path = "/v1/hackathons/timeline/{id}",
params(
("id" = String, Path, description = "Timeline ID")
),
responses(
(status = 200, description = "Timeline deleted successfully", body = ResponseSuccessDto<String>),
(status = 404, description = "Timeline not found", body = ErrorDto),
(status = 500, description = "Internal server error", body = ErrorDto)
),
tag = "Hackathon Timeline"
)]
pub async fn delete_hackathon_timeline(
Extension(state): Extension<AppState>,
Path(id): Path<String>,
) -> impl IntoResponse {
match HackathonService::delete_hackathon_timeline(id, &state).await {
Ok(response) => (axum::http::StatusCode::OK, Json(response)).into_response(),
Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(),
}
}
// Hackathon Submissions routes
#[utoipa::path(
post,
path = "/v1/hackathons/{hackathon_id}/teams/{team_id}/submissions",
params(
("hackathon_id" = String, Path, description = "Hackathon ID"),
("team_id" = String, Path, description = "Team ID")
),
request_body = HackathonSubmissionCreateRequestDto,
responses(
(status = 201, description = "Submission created successfully", body = ResponseSuccessDto<HackathonSubmissionDto>),
(status = 400, description = "Bad request", body = ErrorDto),
(status = 404, description = "Hackathon not found", body = ErrorDto),
(status = 500, description = "Internal server error", body = ErrorDto)
),
tag = "Hackathon Submissions"
)]
pub async fn create_hackathon_submission(
Extension(state): Extension<AppState>,
Path((hackathon_id, team_id)): Path<(String, String)>,
Json(payload): Json<HackathonSubmissionCreateRequestDto>,
) -> impl IntoResponse {
match HackathonService::create_hackathon_submission(hackathon_id, team_id, payload, &state).await {
Ok(response) => (axum::http::StatusCode::CREATED, Json(response)).into_response(),
Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(),
}
}
#[utoipa::path(
get,
path = "/v1/hackathons/{hackathon_id}/submissions",
params(
("hackathon_id" = String, Path, description = "Hackathon ID"),
("page" = Option<i64>, Query, description = "Page number"),
("per_page" = Option<i64>, Query, description = "Items per page"),
("search" = Option<String>, Query, description = "Search keyword"),
("sort_by" = Option<String>, Query, description = "Sort by field"),
("order" = Option<String>, Query, description = "Order ASC or DESC"),
("filter" = Option<String>, Query, description = "Filter value"),
("filter_by" = Option<String>, Query, description = "Field to filter by"),
),
responses(
(status = 200, description = "Submissions retrieved successfully", body = ResponseListSuccessDto<Vec<HackathonSubmissionDto>>),
(status = 500, description = "Internal server error", body = ErrorDto)
),
tag = "Hackathon Submissions"
)]
pub async fn list_hackathon_submissions(
Extension(state): Extension<AppState>,
Path(hackathon_id): Path<String>,
Query(meta): Query<MetaRequestDto>,
) -> impl IntoResponse {
match HackathonService::list_hackathon_submissions(meta, hackathon_id, &state).await {
Ok(response) => (axum::http::StatusCode::OK, Json(response)).into_response(),
Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(),
}
}
#[utoipa::path(
put,
path = "/v1/hackathons/submissions/{id}",
params(
("id" = String, Path, description = "Submission ID")
),
request_body = HackathonSubmissionUpdateRequestDto,
responses(
(status = 200, description = "Submission updated successfully", body = ResponseSuccessDto<HackathonSubmissionDto>),
(status = 400, description = "Bad request", body = ErrorDto),
(status = 404, description = "Submission not found", body = ErrorDto),
(status = 500, description = "Internal server error", body = ErrorDto)
),
tag = "Hackathon Submissions"
)]
pub async fn update_hackathon_submission(
Extension(state): Extension<AppState>,
Path(id): Path<String>,
Json(payload): Json<HackathonSubmissionUpdateRequestDto>,
) -> impl IntoResponse {
match HackathonService::update_hackathon_submission(id, payload, &state).await {
Ok(response) => (axum::http::StatusCode::OK, Json(response)).into_response(),
Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(),
}
}
#[utoipa::path(
post,
path = "/v1/hackathons/submissions/{id}/submit",
params(
("id" = String, Path, description = "Submission ID")
),
responses(
(status = 200, description = "Submission submitted successfully", body = ResponseSuccessDto<HackathonSubmissionDto>),
(status = 404, description = "Submission not found", body = ErrorDto),
(status = 500, description = "Internal server error", body = ErrorDto)
),
tag = "Hackathon Submissions"
)]
pub async fn submit_hackathon_submission(
Extension(state): Extension<AppState>,
Path(id): Path<String>,
) -> impl IntoResponse {
match HackathonService::submit_hackathon_submission(id, &state).await {
Ok(response) => (axum::http::StatusCode::OK, Json(response)).into_response(),
Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(),
}
}
#[utoipa::path(
delete,
path = "/v1/hackathons/submissions/{id}",
params(
("id" = String, Path, description = "Submission ID")
),
responses(
(status = 200, description = "Submission deleted successfully", body = ResponseSuccessDto<String>),
(status = 404, description = "Submission not found", body = ErrorDto),
(status = 500, description = "Internal server error", body = ErrorDto)
),
tag = "Hackathon Submissions"
)]
pub async fn delete_hackathon_submission(
Extension(state): Extension<AppState>,
Path(id): Path<String>,
) -> impl IntoResponse {
match HackathonService::delete_hackathon_submission(id, &state).await {
Ok(response) => (axum::http::StatusCode::OK, Json(response)).into_response(),
Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(),
}
}
pub fn hackathon_routes() -> Router {
Router::new()
// Hackathon routes
.route("/", post(create_hackathon))
.route("/", get(list_hackathons))
.route("/:id", get(get_hackathon))
.route("/:id", put(update_hackathon))
.route("/:id", delete(delete_hackathon))
// Hackathon Events routes
.route("/:hackathon_id/events", post(create_hackathon_event))
.route("/:hackathon_id/events", get(list_hackathon_events))
.route("/events/:id", put(update_hackathon_event))
.route("/events/:id", delete(delete_hackathon_event))
// Hackathon Timeline routes
.route("/:hackathon_id/timeline", post(create_hackathon_timeline))
.route("/:hackathon_id/timeline", get(list_hackathon_timeline))
.route("/timeline/:id", put(update_hackathon_timeline))
.route("/timeline/:id", delete(delete_hackathon_timeline))
// Hackathon Submissions routes
.route("/:hackathon_id/teams/:team_id/submissions", post(create_hackathon_submission))
.route("/:hackathon_id/submissions", get(list_hackathon_submissions))
.route("/submissions/:id", put(update_hackathon_submission))
.route("/submissions/:id/submit", post(submit_hackathon_submission))
.route("/submissions/:id", delete(delete_hackathon_submission))
}
@@ -0,0 +1,401 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use utoipa::{ToSchema, schema};
use validator::Validate;
use crate::v1::hackathon::hackathon_schema::{
HackathonEventType, HackathonEventsSchema, HackathonPhase, HackathonSchema,
HackathonStatus, HackathonSubmissionsSchema, HackathonTimelineSchema,
SubmissionStatus,
};
// Hackathon DTOs
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct HackathonCreateRequestDto {
#[validate(length(min = 1, max = 100, message = "Hackathon name must be between 1 and 100 characters"))]
pub name: String,
#[validate(length(min = 1, max = 1000, message = "Description must be between 1 and 1000 characters"))]
pub description: String,
#[schema(value_type = String, format = DateTime)]
pub start_date: DateTime<Utc>,
#[schema(value_type = String, format = DateTime)]
pub end_date: DateTime<Utc>,
#[schema(value_type = String, format = DateTime)]
pub registration_deadline: DateTime<Utc>,
#[validate(range(min = 1, max = 10000, message = "Max participants must be between 1 and 10000"))]
pub max_participants: Option<u32>,
pub theme: Option<String>,
pub rules: Option<String>,
pub prizes: Option<Vec<PrizeDto>>,
pub organizers: Vec<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct HackathonUpdateRequestDto {
#[validate(length(min = 1, max = 100, message = "Hackathon name must be between 1 and 100 characters"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[validate(length(min = 1, max = 1000, message = "Description must be between 1 and 1000 characters"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[schema(value_type = String, format = DateTime)]
pub start_date: Option<DateTime<Utc>>,
#[schema(value_type = String, format = DateTime)]
pub end_date: Option<DateTime<Utc>>,
#[schema(value_type = String, format = DateTime)]
pub registration_deadline: Option<DateTime<Utc>>,
#[validate(range(min = 1, max = 10000, message = "Max participants must be between 1 and 10000"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub max_participants: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub theme: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rules: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prizes: Option<Vec<PrizeDto>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub organizers: Option<Vec<String>>,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct HackathonDto {
pub id: String,
pub name: String,
pub description: String,
#[schema(value_type = String, format = DateTime)]
pub start_date: DateTime<Utc>,
#[schema(value_type = String, format = DateTime)]
pub end_date: DateTime<Utc>,
#[schema(value_type = String, format = DateTime)]
pub registration_deadline: DateTime<Utc>,
pub max_participants: Option<u32>,
pub status: HackathonStatus,
pub theme: Option<String>,
pub rules: Option<String>,
pub prizes: Option<Vec<PrizeDto>>,
pub organizers: Vec<String>,
pub is_deleted: bool,
pub created_at: Option<String>,
pub updated_at: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct PrizeDto {
#[validate(range(min = 1, message = "Position must be at least 1"))]
pub position: u32,
#[validate(length(min = 1, message = "Prize title cannot be empty"))]
pub title: String,
pub description: Option<String>,
pub value: Option<String>,
}
// Hackathon Events DTOs
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct HackathonEventCreateRequestDto {
#[validate(length(min = 1, message = "Event title cannot be empty"))]
pub title: String,
pub description: Option<String>,
pub event_type: HackathonEventType,
#[schema(value_type = String, format = DateTime)]
pub start_time: DateTime<Utc>,
#[schema(value_type = String, format = DateTime)]
pub end_time: DateTime<Utc>,
pub location: Option<String>,
pub virtual_link: Option<String>,
#[validate(range(min = 1, message = "Max attendees must be at least 1"))]
pub max_attendees: Option<u32>,
pub is_mandatory: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct HackathonEventUpdateRequestDto {
#[validate(length(min = 1, message = "Event title cannot be empty"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub event_type: Option<HackathonEventType>,
#[serde(skip_serializing_if = "Option::is_none")]
#[schema(value_type = String, format = DateTime)]
pub start_time: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[schema(value_type = String, format = DateTime)]
pub end_time: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub location: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub virtual_link: Option<String>,
#[validate(range(min = 1, message = "Max attendees must be at least 1"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub max_attendees: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_mandatory: Option<bool>,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct HackathonEventDto {
pub id: String,
pub hackathon_id: String,
pub title: String,
pub description: Option<String>,
pub event_type: HackathonEventType,
#[schema(value_type = String, format = DateTime)]
pub start_time: DateTime<Utc>,
#[schema(value_type = String, format = DateTime)]
pub end_time: DateTime<Utc>,
pub location: Option<String>,
pub virtual_link: Option<String>,
pub max_attendees: Option<u32>,
pub is_mandatory: bool,
pub is_deleted: bool,
pub created_at: Option<String>,
pub updated_at: Option<String>,
}
// Hackathon Timeline DTOs
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct HackathonTimelineCreateRequestDto {
pub phase: HackathonPhase,
#[validate(length(min = 1, message = "Timeline title cannot be empty"))]
pub title: String,
pub description: Option<String>,
#[schema(value_type = String, format = DateTime)]
pub start_date: DateTime<Utc>,
#[schema(value_type = String, format = DateTime)]
pub end_date: DateTime<Utc>,
pub is_active: bool,
#[validate(range(min = 0, message = "Order must be non-negative"))]
pub order: u32,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct HackathonTimelineUpdateRequestDto {
#[serde(skip_serializing_if = "Option::is_none")]
pub phase: Option<HackathonPhase>,
#[validate(length(min = 1, message = "Timeline title cannot be empty"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[schema(value_type = String, format = DateTime)]
pub start_date: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[schema(value_type = String, format = DateTime)]
pub end_date: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_active: Option<bool>,
#[validate(range(min = 0, message = "Order must be non-negative"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub order: Option<u32>,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct HackathonTimelineDto {
pub id: String,
pub hackathon_id: String,
pub phase: HackathonPhase,
pub title: String,
pub description: Option<String>,
#[schema(value_type = String, format = DateTime)]
pub start_date: DateTime<Utc>,
#[schema(value_type = String, format = DateTime)]
pub end_date: DateTime<Utc>,
pub is_active: bool,
pub order: u32,
pub is_deleted: bool,
pub created_at: Option<String>,
pub updated_at: Option<String>,
}
// Hackathon Submissions DTOs
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct HackathonSubmissionCreateRequestDto {
#[validate(length(min = 1, message = "Project name cannot be empty"))]
pub project_name: String,
#[validate(length(min = 1, message = "Description cannot be empty"))]
pub description: String,
pub repository_url: Option<String>,
pub demo_url: Option<String>,
pub slides_url: Option<String>,
pub technologies: Vec<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct HackathonSubmissionUpdateRequestDto {
#[validate(length(min = 1, message = "Project name cannot be empty"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub project_name: Option<String>,
#[validate(length(min = 1, message = "Description cannot be empty"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub repository_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub demo_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub slides_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub technologies: Option<Vec<String>>,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct HackathonSubmissionDto {
pub id: String,
pub hackathon_id: String,
pub team_id: String,
pub project_name: String,
pub description: String,
pub repository_url: Option<String>,
pub demo_url: Option<String>,
pub slides_url: Option<String>,
pub technologies: Vec<String>,
pub submission_status: SubmissionStatus,
#[schema(value_type = String, format = DateTime)]
pub submitted_at: DateTime<Utc>,
pub is_deleted: bool,
pub created_at: Option<String>,
pub updated_at: Option<String>,
}
// Query DTOs
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct HackathonQueryDto {
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<HackathonStatus>,
#[serde(skip_serializing_if = "Option::is_none")]
pub organizer_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub offset: Option<u32>,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct HackathonEventQueryDto {
pub hackathon_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub event_type: Option<HackathonEventType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub offset: Option<u32>,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct HackathonTimelineQueryDto {
pub hackathon_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub phase: Option<HackathonPhase>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_active: Option<bool>,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct HackathonSubmissionQueryDto {
pub hackathon_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub team_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub submission_status: Option<SubmissionStatus>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub offset: Option<u32>,
}
// Conversion implementations
impl From<HackathonSchema> for HackathonDto {
fn from(schema: HackathonSchema) -> Self {
Self {
id: schema.id.id.to_raw(),
name: schema.name,
description: schema.description,
start_date: schema.start_date,
end_date: schema.end_date,
registration_deadline: schema.registration_deadline,
max_participants: schema.max_participants,
status: schema.status,
theme: schema.theme,
rules: schema.rules,
prizes: schema.prizes.map(|prizes| {
prizes
.into_iter()
.map(|p| PrizeDto {
position: p.position,
title: p.title,
description: p.description,
value: p.value,
})
.collect()
}),
organizers: schema.organizers,
is_deleted: schema.is_deleted,
created_at: schema.created_at,
updated_at: schema.updated_at,
}
}
}
impl From<HackathonEventsSchema> for HackathonEventDto {
fn from(schema: HackathonEventsSchema) -> Self {
Self {
id: schema.id.id.to_raw(),
hackathon_id: schema.hackathon_id.id.to_raw(),
title: schema.title,
description: schema.description,
event_type: schema.event_type,
start_time: schema.start_time,
end_time: schema.end_time,
location: schema.location,
virtual_link: schema.virtual_link,
max_attendees: schema.max_attendees,
is_mandatory: schema.is_mandatory,
is_deleted: schema.is_deleted,
created_at: schema.created_at,
updated_at: schema.updated_at,
}
}
}
impl From<HackathonTimelineSchema> for HackathonTimelineDto {
fn from(schema: HackathonTimelineSchema) -> Self {
Self {
id: schema.id.id.to_raw(),
hackathon_id: schema.hackathon_id.id.to_raw(),
phase: schema.phase,
title: schema.title,
description: schema.description,
start_date: schema.start_date,
end_date: schema.end_date,
is_active: schema.is_active,
order: schema.order,
is_deleted: schema.is_deleted,
created_at: schema.created_at,
updated_at: schema.updated_at,
}
}
}
impl From<HackathonSubmissionsSchema> for HackathonSubmissionDto {
fn from(schema: HackathonSubmissionsSchema) -> Self {
Self {
id: schema.id.id.to_raw(),
hackathon_id: schema.hackathon_id.id.to_raw(),
team_id: schema.team_id.id.to_raw(),
project_name: schema.project_name,
description: schema.description,
repository_url: schema.repository_url,
demo_url: schema.demo_url,
slides_url: schema.slides_url,
technologies: schema.technologies,
submission_status: schema.submission_status,
submitted_at: schema.submitted_at,
is_deleted: schema.is_deleted,
created_at: schema.created_at,
updated_at: schema.updated_at,
}
}
}
@@ -0,0 +1,600 @@
use super::hackathon_dto::{
HackathonCreateRequestDto, HackathonEventCreateRequestDto,
HackathonEventUpdateRequestDto, HackathonSubmissionCreateRequestDto,
HackathonSubmissionUpdateRequestDto, HackathonTimelineCreateRequestDto,
HackathonTimelineUpdateRequestDto, HackathonUpdateRequestDto,
};
use super::hackathon_schema::{
HackathonEventsSchema, HackathonSchema, HackathonSubmissionsSchema, HackathonTimelineSchema,
Prize,
};
use imphnen_libs::ResourceEnum;
use anyhow::{Result, anyhow, bail};
use imphnen_libs::AppState;
use imphnen_utils::{QueryListBuilder, get_iso_date};
use std::collections::HashMap;
use surrealdb::sql::Thing;
use tracing::{instrument, info};
#[derive(Clone)]
pub struct HackathonRepository<'a> {
pub state: &'a AppState,
}
impl<'a> HackathonRepository<'a> {
pub fn new(state: &'a AppState) -> Self {
Self { state }
}
}
// Hackathon CRUD operations
impl<'a> HackathonRepository<'a> {
#[instrument(skip(self, hackathon), err)]
pub async fn create_hackathon(&self, hackathon: HackathonCreateRequestDto) -> Result<HackathonSchema> {
let table = ResourceEnum::Hackathons.to_string();
let id = surrealdb::Uuid::new_v4().to_string();
let prizes: Option<Vec<Prize>> = hackathon.prizes.map(|p| {
p.into_iter()
.map(|prize| Prize {
position: prize.position,
title: prize.title,
description: prize.description,
value: prize.value,
})
.collect()
});
let schema = HackathonSchema {
id: Thing::from((table.clone(), id.clone())),
name: hackathon.name,
description: hackathon.description,
start_date: hackathon.start_date,
end_date: hackathon.end_date,
registration_deadline: hackathon.registration_deadline,
max_participants: hackathon.max_participants,
status: super::hackathon_schema::HackathonStatus::Draft,
theme: hackathon.theme,
rules: hackathon.rules,
prizes,
organizers: hackathon.organizers,
is_deleted: false,
created_at: Some(get_iso_date()),
updated_at: Some(get_iso_date()),
};
info!(query = %format!("CREATE {}:{}", table, id), "Executing SurrealDB query");
let record: Option<HackathonSchema> = self
.state.surrealdb_ws
.create((table, id))
.content(schema.clone())
.await?;
match record {
Some(h) => Ok(h),
None => bail!("Failed to create hackathon"),
}
}
#[instrument(skip(self, id), err)]
pub async fn get_hackathon_by_id(&self, id: String) -> Result<HackathonSchema> {
let table = ResourceEnum::Hackathons.to_string();
info!(query = %format!("SELECT * FROM {} WHERE id = '{}'", table, id), "Executing SurrealDB query");
let record: Option<HackathonSchema> = self
.state
.surrealdb_ws
.select((table, id))
.await?;
match record {
Some(h) => {
if h.is_deleted {
bail!("Hackathon not found");
}
Ok(h)
}
None => bail!("Hackathon not found"),
}
}
#[instrument(skip(self, meta), err)]
pub async fn list_hackathons(&self, meta: imphnen_libs::MetaRequestDto) -> Result<imphnen_libs::ResponseListSuccessDto<Vec<HackathonSchema>>> {
let table = ResourceEnum::Hackathons.to_string();
let builder = QueryListBuilder::new(&self.state.surrealdb_ws, &table, &meta)
.with_condition("is_deleted = false")
.search_field("name")
.select_fields(vec!["*"]);
let result = builder.build().await?;
Ok(result)
}
#[instrument(skip(self, id, updates), err)]
pub async fn update_hackathon(&self, id: String, updates: HackathonUpdateRequestDto) -> Result<HackathonSchema> {
let table = ResourceEnum::Hackathons.to_string();
// First get the existing hackathon
let mut existing = self.get_hackathon_by_id(id.clone()).await?;
// Apply updates
if let Some(name) = updates.name {
existing.name = name;
}
if let Some(description) = updates.description {
existing.description = description;
}
if let Some(start_date) = updates.start_date {
existing.start_date = start_date;
}
if let Some(end_date) = updates.end_date {
existing.end_date = end_date;
}
if let Some(registration_deadline) = updates.registration_deadline {
existing.registration_deadline = registration_deadline;
}
if let Some(max_participants) = updates.max_participants {
existing.max_participants = Some(max_participants);
}
if let Some(theme) = updates.theme {
existing.theme = Some(theme);
}
if let Some(rules) = updates.rules {
existing.rules = Some(rules);
}
if let Some(prizes) = updates.prizes {
let prizes_schema: Vec<Prize> = prizes
.into_iter()
.map(|p| Prize {
position: p.position,
title: p.title,
description: p.description,
value: p.value,
})
.collect();
existing.prizes = Some(prizes_schema);
}
if let Some(organizers) = updates.organizers {
existing.organizers = organizers;
}
existing.updated_at = Some(get_iso_date());
info!(query = %format!("UPDATE {} SET ... WHERE id = '{}'", table, id), "Executing SurrealDB query");
let record: Option<HackathonSchema> = self
.state.surrealdb_ws
.update((table, id))
.content(existing.clone())
.await?;
match record {
Some(h) => Ok(h),
None => bail!("Failed to update hackathon"),
}
}
#[instrument(skip(self, id), err)]
pub async fn delete_hackathon(&self, id: String) -> Result<String> {
let table = ResourceEnum::Hackathons.to_string();
// Soft delete by setting is_deleted = true
let updates: HashMap<String, serde_json::Value> = HashMap::from([
("is_deleted".to_string(), true.into()),
("updated_at".to_string(), get_iso_date().into()),
]);
info!(query = %format!("UPDATE {} SET is_deleted = true WHERE id = '{}'", table, id), "Executing SurrealDB query");
let record: Option<HackathonSchema> = self
.state.surrealdb_ws
.update((table, id))
.merge(serde_json::to_value(updates)?)
.await?;
match record {
Some(_) => Ok("Hackathon deleted successfully".to_string()),
None => bail!("Failed to delete hackathon"),
}
}
}
// Hackathon Events CRUD operations
impl<'a> HackathonRepository<'a> {
#[instrument(skip(self, hackathon_id, event), err)]
pub async fn create_hackathon_event(&self, hackathon_id: String, event: HackathonEventCreateRequestDto) -> Result<HackathonEventsSchema> {
let table = ResourceEnum::HackathonEvents.to_string();
let id = surrealdb::Uuid::new_v4().to_string();
let schema = HackathonEventsSchema {
id: Thing::from((table.clone(), id.clone())),
hackathon_id: Thing::from(("app_hackathons".to_string(), hackathon_id)),
title: event.title,
description: event.description,
event_type: event.event_type,
start_time: event.start_time,
end_time: event.end_time,
location: event.location,
virtual_link: event.virtual_link,
max_attendees: event.max_attendees,
is_mandatory: event.is_mandatory,
is_deleted: false,
created_at: Some(get_iso_date()),
updated_at: Some(get_iso_date()),
};
info!(query = %format!("CREATE {}:{}", table, id), "Executing SurrealDB query");
let record: Option<HackathonEventsSchema> = self
.state.surrealdb_ws
.create((table, id))
.content(schema.clone())
.await?;
match record {
Some(e) => Ok(e),
None => bail!("Failed to create hackathon event"),
}
}
#[instrument(skip(self, meta, hackathon_id), err)]
pub async fn list_hackathon_events(&self, meta: imphnen_libs::MetaRequestDto, hackathon_id: String) -> Result<imphnen_libs::ResponseListSuccessDto<Vec<HackathonEventsSchema>>> {
let table = ResourceEnum::HackathonEvents.to_string();
let builder = QueryListBuilder::new(&self.state.surrealdb_ws, &table, &meta)
.with_condition("is_deleted = false")
.with_condition(&format!("hackathon_id = app_hackathons:{}", hackathon_id))
.search_field("title")
.select_fields(vec!["*"]);
let result = builder.build().await?;
Ok(result)
}
#[instrument(skip(self, id, updates), err)]
pub async fn update_hackathon_event(&self, id: String, updates: HackathonEventUpdateRequestDto) -> Result<HackathonEventsSchema> {
let table = ResourceEnum::HackathonEvents.to_string();
// Get existing event
let existing: Option<HackathonEventsSchema> = self.state.surrealdb_ws.select((table.clone(), id.clone())).await?;
let mut existing = existing.ok_or_else(|| anyhow!("Event not found"))?;
if existing.is_deleted {
bail!("Event not found");
}
// Apply updates
if let Some(title) = updates.title {
existing.title = title;
}
if let Some(description) = updates.description {
existing.description = Some(description);
}
if let Some(event_type) = updates.event_type {
existing.event_type = event_type;
}
if let Some(start_time) = updates.start_time {
existing.start_time = start_time;
}
if let Some(end_time) = updates.end_time {
existing.end_time = end_time;
}
if let Some(location) = updates.location {
existing.location = Some(location);
}
if let Some(virtual_link) = updates.virtual_link {
existing.virtual_link = Some(virtual_link);
}
if let Some(max_attendees) = updates.max_attendees {
existing.max_attendees = Some(max_attendees);
}
if let Some(is_mandatory) = updates.is_mandatory {
existing.is_mandatory = is_mandatory;
}
existing.updated_at = Some(get_iso_date());
info!(query = %format!("UPDATE {} SET ... WHERE id = '{}'", table, id), "Executing SurrealDB query");
let record: Option<HackathonEventsSchema> = self
.state.surrealdb_ws
.update((table, id))
.content(existing.clone())
.await?;
match record {
Some(e) => Ok(e),
None => bail!("Failed to update hackathon event"),
}
}
#[instrument(skip(self, id), err)]
pub async fn delete_hackathon_event(&self, id: String) -> Result<String> {
let table = ResourceEnum::HackathonEvents.to_string();
let updates: HashMap<String, serde_json::Value> = HashMap::from([
("is_deleted".to_string(), true.into()),
("updated_at".to_string(), get_iso_date().into()),
]);
info!(query = %format!("UPDATE {} SET is_deleted = true WHERE id = '{}'", table, id), "Executing SurrealDB query");
let record: Option<HackathonEventsSchema> = self
.state.surrealdb_ws
.update((table, id))
.merge(serde_json::to_value(updates)?)
.await?;
match record {
Some(_) => Ok("Event deleted successfully".to_string()),
None => bail!("Failed to delete event"),
}
}
}
// Hackathon Timeline CRUD operations
impl<'a> HackathonRepository<'a> {
#[instrument(skip(self, hackathon_id, timeline), err)]
pub async fn create_hackathon_timeline(&self, hackathon_id: String, timeline: HackathonTimelineCreateRequestDto) -> Result<HackathonTimelineSchema> {
let table = ResourceEnum::HackathonTimeline.to_string();
let id = surrealdb::Uuid::new_v4().to_string();
let schema = HackathonTimelineSchema {
id: Thing::from((table.clone(), id.clone())),
hackathon_id: Thing::from(("app_hackathons".to_string(), hackathon_id)),
phase: timeline.phase,
title: timeline.title,
description: timeline.description,
start_date: timeline.start_date,
end_date: timeline.end_date,
is_active: timeline.is_active,
order: timeline.order,
is_deleted: false,
created_at: Some(get_iso_date()),
updated_at: Some(get_iso_date()),
};
info!(query = %format!("CREATE {}:{}", table, id), "Executing SurrealDB query");
let record: Option<HackathonTimelineSchema> = self
.state.surrealdb_ws
.create((table, id))
.content(schema.clone())
.await?;
match record {
Some(t) => Ok(t),
None => bail!("Failed to create hackathon timeline"),
}
}
#[instrument(skip(self, meta, hackathon_id), err)]
pub async fn list_hackathon_timeline(&self, meta: imphnen_libs::MetaRequestDto, hackathon_id: String) -> Result<imphnen_libs::ResponseListSuccessDto<Vec<HackathonTimelineSchema>>> {
let table = ResourceEnum::HackathonTimeline.to_string();
let builder = QueryListBuilder::new(&self.state.surrealdb_ws, &table, &meta)
.with_condition("is_deleted = false")
.with_condition(&format!("hackathon_id = app_hackathons:{}", hackathon_id))
.search_field("title")
.select_fields(vec!["*"]);
let result = builder.build().await?;
Ok(result)
}
#[instrument(skip(self, id, updates), err)]
pub async fn update_hackathon_timeline(&self, id: String, updates: HackathonTimelineUpdateRequestDto) -> Result<HackathonTimelineSchema> {
let table = ResourceEnum::HackathonTimeline.to_string();
let existing: Option<HackathonTimelineSchema> = self.state.surrealdb_ws.select((table.clone(), id.clone())).await?;
let mut existing = existing.ok_or_else(|| anyhow!("Timeline not found"))?;
if existing.is_deleted {
bail!("Timeline not found");
}
// Apply updates
if let Some(phase) = updates.phase {
existing.phase = phase;
}
if let Some(title) = updates.title {
existing.title = title;
}
if let Some(description) = updates.description {
existing.description = Some(description);
}
if let Some(start_date) = updates.start_date {
existing.start_date = start_date;
}
if let Some(end_date) = updates.end_date {
existing.end_date = end_date;
}
if let Some(is_active) = updates.is_active {
existing.is_active = is_active;
}
if let Some(order) = updates.order {
existing.order = order;
}
existing.updated_at = Some(get_iso_date());
info!(query = %format!("UPDATE {} SET ... WHERE id = '{}'", table, id), "Executing SurrealDB query");
let record: Option<HackathonTimelineSchema> = self
.state.surrealdb_ws
.update((table, id))
.content(existing.clone())
.await?;
match record {
Some(t) => Ok(t),
None => bail!("Failed to update hackathon timeline"),
}
}
#[instrument(skip(self, id), err)]
pub async fn delete_hackathon_timeline(&self, id: String) -> Result<String> {
let table = ResourceEnum::HackathonTimeline.to_string();
let updates: HashMap<String, serde_json::Value> = HashMap::from([
("is_deleted".to_string(), true.into()),
("updated_at".to_string(), get_iso_date().into()),
]);
info!(query = %format!("UPDATE {} SET is_deleted = true WHERE id = '{}'", table, id), "Executing SurrealDB query");
let record: Option<HackathonTimelineSchema> = self
.state.surrealdb_ws
.update((table, id))
.merge(serde_json::to_value(updates)?)
.await?;
match record {
Some(_) => Ok("Timeline deleted successfully".to_string()),
None => bail!("Failed to delete timeline"),
}
}
}
// Hackathon Submissions CRUD operations
impl<'a> HackathonRepository<'a> {
#[instrument(skip(self, hackathon_id, team_id, submission), err)]
pub async fn create_hackathon_submission(&self, hackathon_id: String, team_id: String, submission: HackathonSubmissionCreateRequestDto) -> Result<HackathonSubmissionsSchema> {
let table = ResourceEnum::HackathonSubmissions.to_string();
let id = surrealdb::Uuid::new_v4().to_string();
let schema = HackathonSubmissionsSchema {
id: Thing::from((table.clone(), id.clone())),
hackathon_id: Thing::from(("app_hackathons".to_string(), hackathon_id)),
team_id: Thing::from(("app_teams".to_string(), team_id)),
project_name: submission.project_name,
description: submission.description,
repository_url: submission.repository_url,
demo_url: submission.demo_url,
slides_url: submission.slides_url,
technologies: submission.technologies,
submission_status: super::hackathon_schema::SubmissionStatus::Draft,
submitted_at: chrono::Utc::now(),
is_deleted: false,
created_at: Some(get_iso_date()),
updated_at: Some(get_iso_date()),
};
info!(query = %format!("CREATE {}:{}", table, id), "Executing SurrealDB query");
let record: Option<HackathonSubmissionsSchema> = self
.state.surrealdb_ws
.create((table, id))
.content(schema.clone())
.await?;
match record {
Some(s) => Ok(s),
None => bail!("Failed to create hackathon submission"),
}
}
#[instrument(skip(self, meta, hackathon_id), err)]
pub async fn list_hackathon_submissions(&self, meta: imphnen_libs::MetaRequestDto, hackathon_id: String) -> Result<imphnen_libs::ResponseListSuccessDto<Vec<HackathonSubmissionsSchema>>> {
let table = ResourceEnum::HackathonSubmissions.to_string();
let builder = QueryListBuilder::new(&self.state.surrealdb_ws, &table, &meta)
.with_condition("is_deleted = false")
.with_condition(&format!("hackathon_id = app_hackathons:{}", hackathon_id))
.search_field("project_name")
.select_fields(vec!["*"]);
let result = builder.build().await?;
Ok(result)
}
#[instrument(skip(self, id, updates), err)]
pub async fn update_hackathon_submission(&self, id: String, updates: HackathonSubmissionUpdateRequestDto) -> Result<HackathonSubmissionsSchema> {
let table = ResourceEnum::HackathonSubmissions.to_string();
let existing: Option<HackathonSubmissionsSchema> = self.state.surrealdb_ws.select((table.clone(), id.clone())).await?;
let mut existing = existing.ok_or_else(|| anyhow!("Submission not found"))?;
if existing.is_deleted {
bail!("Submission not found");
}
// Apply updates
if let Some(project_name) = updates.project_name {
existing.project_name = project_name;
}
if let Some(description) = updates.description {
existing.description = description;
}
if let Some(repository_url) = updates.repository_url {
existing.repository_url = Some(repository_url);
}
if let Some(demo_url) = updates.demo_url {
existing.demo_url = Some(demo_url);
}
if let Some(slides_url) = updates.slides_url {
existing.slides_url = Some(slides_url);
}
if let Some(technologies) = updates.technologies {
existing.technologies = technologies;
}
existing.updated_at = Some(get_iso_date());
info!(query = %format!("UPDATE {} SET ... WHERE id = '{}'", table, id), "Executing SurrealDB query");
let record: Option<HackathonSubmissionsSchema> = self
.state.surrealdb_ws
.update((table, id))
.content(existing.clone())
.await?;
match record {
Some(s) => Ok(s),
None => bail!("Failed to update hackathon submission"),
}
}
#[instrument(skip(self, id), err)]
pub async fn submit_hackathon_submission(&self, id: String) -> Result<HackathonSubmissionsSchema> {
let table = ResourceEnum::HackathonSubmissions.to_string();
let existing: Option<HackathonSubmissionsSchema> = self.state.surrealdb_ws.select((table.clone(), id.clone())).await?;
let mut existing = existing.ok_or_else(|| anyhow!("Submission not found"))?;
if existing.is_deleted {
bail!("Submission not found");
}
existing.submission_status = super::hackathon_schema::SubmissionStatus::Submitted;
existing.submitted_at = chrono::Utc::now();
existing.updated_at = Some(get_iso_date());
info!(query = %format!("UPDATE {} SET submission_status = 'Submitted' WHERE id = '{}'", table, id), "Executing SurrealDB query");
let record: Option<HackathonSubmissionsSchema> = self
.state.surrealdb_ws
.update((table, id))
.content(existing.clone())
.await?;
match record {
Some(s) => Ok(s),
None => bail!("Failed to submit hackathon submission"),
}
}
#[instrument(skip(self, id), err)]
pub async fn delete_hackathon_submission(&self, id: String) -> Result<String> {
let table = ResourceEnum::HackathonSubmissions.to_string();
let updates: HashMap<String, serde_json::Value> = HashMap::from([
("is_deleted".to_string(), true.into()),
("updated_at".to_string(), get_iso_date().into()),
]);
info!(query = %format!("UPDATE {} SET is_deleted = true WHERE id = '{}'", table, id), "Executing SurrealDB query");
let record: Option<HackathonSubmissionsSchema> = self
.state.surrealdb_ws
.update((table, id))
.merge(serde_json::to_value(updates)?)
.await?;
match record {
Some(_) => Ok("Submission deleted successfully".to_string()),
None => bail!("Failed to delete submission"),
}
}
}
@@ -0,0 +1,222 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use surrealdb::sql::Thing;
use imphnen_utils::make_thing;
use imphnen_utils::get_iso_date;
use imphnen_libs::ResourceEnum;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct HackathonSchema {
pub id: Thing,
pub name: String,
pub description: String,
pub start_date: DateTime<Utc>,
pub end_date: DateTime<Utc>,
pub registration_deadline: DateTime<Utc>,
pub max_participants: Option<u32>,
pub status: HackathonStatus,
pub theme: Option<String>,
pub rules: Option<String>,
pub prizes: Option<Vec<Prize>>,
pub organizers: Vec<String>, // User IDs
pub is_deleted: bool,
pub created_at: Option<String>,
pub updated_at: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct HackathonEventsSchema {
pub id: Thing,
pub hackathon_id: Thing,
pub title: String,
pub description: Option<String>,
pub event_type: HackathonEventType,
pub start_time: DateTime<Utc>,
pub end_time: DateTime<Utc>,
pub location: Option<String>,
pub virtual_link: Option<String>,
pub max_attendees: Option<u32>,
pub is_mandatory: bool,
pub is_deleted: bool,
pub created_at: Option<String>,
pub updated_at: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct HackathonTimelineSchema {
pub id: Thing,
pub hackathon_id: Thing,
pub phase: HackathonPhase,
pub title: String,
pub description: Option<String>,
pub start_date: DateTime<Utc>,
pub end_date: DateTime<Utc>,
pub is_active: bool,
pub order: u32,
pub is_deleted: bool,
pub created_at: Option<String>,
pub updated_at: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct HackathonSubmissionsSchema {
pub id: Thing,
pub hackathon_id: Thing,
pub team_id: Thing,
pub project_name: String,
pub description: String,
pub repository_url: Option<String>,
pub demo_url: Option<String>,
pub slides_url: Option<String>,
pub technologies: Vec<String>,
pub submission_status: SubmissionStatus,
pub submitted_at: DateTime<Utc>,
pub is_deleted: bool,
pub created_at: Option<String>,
pub updated_at: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Prize {
pub position: u32,
pub title: String,
pub description: Option<String>,
pub value: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
pub enum HackathonStatus {
Draft,
RegistrationOpen,
RegistrationClosed,
InProgress,
Judging,
Completed,
Cancelled,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
pub enum HackathonEventType {
Workshop,
Keynote,
Networking,
Judging,
Ceremony,
Other,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
pub enum HackathonPhase {
Registration,
Ideation,
Development,
Submission,
Judging,
Awards,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
pub enum SubmissionStatus {
Draft,
Submitted,
UnderReview,
Shortlisted,
Winner,
Rejected,
}
impl Default for HackathonSchema {
fn default() -> Self {
HackathonSchema {
id: make_thing(
&ResourceEnum::Hackathons.to_string(),
&surrealdb::Uuid::new_v4().to_string(),
),
name: String::new(),
description: String::new(),
start_date: Utc::now(),
end_date: Utc::now(),
registration_deadline: Utc::now(),
max_participants: None,
status: HackathonStatus::Draft,
theme: None,
rules: None,
prizes: None,
organizers: vec![],
is_deleted: false,
created_at: Some(get_iso_date()),
updated_at: Some(get_iso_date()),
}
}
}
impl Default for HackathonEventsSchema {
fn default() -> Self {
HackathonEventsSchema {
id: make_thing(
&ResourceEnum::HackathonEvents.to_string(),
&surrealdb::Uuid::new_v4().to_string(),
),
hackathon_id: Thing::from(("app_hackathons".to_string(), surrealdb::sql::Id::rand())),
title: String::new(),
description: None,
event_type: HackathonEventType::Other,
start_time: Utc::now(),
end_time: Utc::now(),
location: None,
virtual_link: None,
max_attendees: None,
is_mandatory: false,
is_deleted: false,
created_at: Some(get_iso_date()),
updated_at: Some(get_iso_date()),
}
}
}
impl Default for HackathonTimelineSchema {
fn default() -> Self {
HackathonTimelineSchema {
id: make_thing(
&ResourceEnum::HackathonTimeline.to_string(),
&surrealdb::Uuid::new_v4().to_string(),
),
hackathon_id: Thing::from(("app_hackathons".to_string(), surrealdb::sql::Id::rand())),
phase: HackathonPhase::Registration,
title: String::new(),
description: None,
start_date: Utc::now(),
end_date: Utc::now(),
is_active: false,
order: 0,
is_deleted: false,
created_at: Some(get_iso_date()),
updated_at: Some(get_iso_date()),
}
}
}
impl Default for HackathonSubmissionsSchema {
fn default() -> Self {
HackathonSubmissionsSchema {
id: make_thing(
&ResourceEnum::HackathonSubmissions.to_string(),
&surrealdb::Uuid::new_v4().to_string(),
),
hackathon_id: Thing::from(("app_hackathons".to_string(), surrealdb::sql::Id::rand())),
team_id: Thing::from(("app_teams".to_string(), surrealdb::sql::Id::rand())),
project_name: String::new(),
description: String::new(),
repository_url: None,
demo_url: None,
slides_url: None,
technologies: vec![],
submission_status: SubmissionStatus::Draft,
submitted_at: Utc::now(),
is_deleted: false,
created_at: Some(get_iso_date()),
updated_at: Some(get_iso_date()),
}
}
}
@@ -0,0 +1,829 @@
use std::pin::Pin;
use std::future::Future;
use super::hackathon_dto::{
HackathonCreateRequestDto, HackathonDto, HackathonEventCreateRequestDto, HackathonEventDto,
HackathonEventUpdateRequestDto, HackathonSubmissionCreateRequestDto,
HackathonSubmissionDto, HackathonSubmissionUpdateRequestDto, HackathonTimelineCreateRequestDto,
HackathonTimelineDto, HackathonTimelineUpdateRequestDto, HackathonUpdateRequestDto,
};
use super::hackathon_repository::HackathonRepository;
use crate::{AppState, ResponseSuccessDto, ErrorDto};
use imphnen_utils::{validator::validate_request};
use imphnen_libs::{MetaRequestDto, ResponseListSuccessDto};
use axum::http::StatusCode;
use tracing::error;
pub trait HackathonServiceTrait: Send + Sync + 'static {
// Hackathon operations
fn create_hackathon(
payload: HackathonCreateRequestDto,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<HackathonDto>, ErrorDto>> + Send>>;
fn get_hackathon(
id: String,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<HackathonDto>, ErrorDto>> + Send>>;
fn list_hackathons(
meta: MetaRequestDto,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseListSuccessDto<Vec<HackathonDto>>, ErrorDto>> + Send>>;
fn update_hackathon(
id: String,
payload: HackathonUpdateRequestDto,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<HackathonDto>, ErrorDto>> + Send>>;
fn delete_hackathon(
id: String,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<String>, ErrorDto>> + Send>>;
// Hackathon Events operations
fn create_hackathon_event(
hackathon_id: String,
payload: HackathonEventCreateRequestDto,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<HackathonEventDto>, ErrorDto>> + Send>>;
fn list_hackathon_events(
meta: MetaRequestDto,
hackathon_id: String,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseListSuccessDto<Vec<HackathonEventDto>>, ErrorDto>> + Send>>;
fn update_hackathon_event(
id: String,
payload: HackathonEventUpdateRequestDto,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<HackathonEventDto>, ErrorDto>> + Send>>;
fn delete_hackathon_event(
id: String,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<String>, ErrorDto>> + Send>>;
// Hackathon Timeline operations
fn create_hackathon_timeline(
hackathon_id: String,
payload: HackathonTimelineCreateRequestDto,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<HackathonTimelineDto>, ErrorDto>> + Send>>;
fn list_hackathon_timeline(
meta: MetaRequestDto,
hackathon_id: String,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseListSuccessDto<Vec<HackathonTimelineDto>>, ErrorDto>> + Send>>;
fn update_hackathon_timeline(
id: String,
payload: HackathonTimelineUpdateRequestDto,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<HackathonTimelineDto>, ErrorDto>> + Send>>;
fn delete_hackathon_timeline(
id: String,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<String>, ErrorDto>> + Send>>;
// Hackathon Submissions operations
fn create_hackathon_submission(
hackathon_id: String,
team_id: String,
payload: HackathonSubmissionCreateRequestDto,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<HackathonSubmissionDto>, ErrorDto>> + Send>>;
fn list_hackathon_submissions(
meta: MetaRequestDto,
hackathon_id: String,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseListSuccessDto<Vec<HackathonSubmissionDto>>, ErrorDto>> + Send>>;
fn update_hackathon_submission(
id: String,
payload: HackathonSubmissionUpdateRequestDto,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<HackathonSubmissionDto>, ErrorDto>> + Send>>;
fn submit_hackathon_submission(
id: String,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<HackathonSubmissionDto>, ErrorDto>> + Send>>;
fn delete_hackathon_submission(
id: String,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<String>, ErrorDto>> + Send>>;
}
#[derive(Clone)]
pub struct HackathonService;
impl HackathonServiceTrait for HackathonService {
fn create_hackathon(
payload: HackathonCreateRequestDto,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<HackathonDto>, ErrorDto>> + Send>> {
let payload = payload;
let state = state.to_owned();
Box::pin(async move {
// Validate request
if let Err((_, error_message)) = 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 })),
});
}
// Business logic validation
if payload.end_date <= payload.start_date {
return Err(ErrorDto {
status: StatusCode::BAD_REQUEST.as_u16(),
message: "End date must be after start date".to_string(),
details: None,
});
}
if payload.registration_deadline >= payload.start_date {
return Err(ErrorDto {
status: StatusCode::BAD_REQUEST.as_u16(),
message: "Registration deadline must be before start date".to_string(),
details: None,
});
}
if payload.organizers.is_empty() {
return Err(ErrorDto {
status: StatusCode::BAD_REQUEST.as_u16(),
message: "At least one organizer is required".to_string(),
details: None,
});
}
let repo = HackathonRepository::new(&state);
match repo.create_hackathon(payload).await {
Ok(hackathon) => {
let dto = HackathonDto::from(hackathon);
Ok(ResponseSuccessDto { data: dto })
}
Err(e) => {
error!("Failed to create hackathon: {}", e);
Err(ErrorDto {
status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
message: "Failed to create hackathon".to_string(),
details: None,
})
}
}
})
}
fn get_hackathon(
id: String,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<HackathonDto>, ErrorDto>> + Send>> {
let state = state.to_owned();
Box::pin(async move {
let repo = HackathonRepository::new(&state);
match repo.get_hackathon_by_id(id).await {
Ok(hackathon) => {
let dto = HackathonDto::from(hackathon);
Ok(ResponseSuccessDto { data: dto })
}
Err(e) => {
error!("Failed to get hackathon: {}", e);
Err(ErrorDto {
status: StatusCode::NOT_FOUND.as_u16(),
message: "Hackathon not found".to_string(),
details: None,
})
}
}
})
}
fn list_hackathons(
meta: MetaRequestDto,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseListSuccessDto<Vec<HackathonDto>>, ErrorDto>> + Send>> {
let state = state.to_owned();
Box::pin(async move {
let repo = HackathonRepository::new(&state);
match repo.list_hackathons(meta).await {
Ok(result) => {
let dtos: Vec<HackathonDto> = result.data.into_iter().map(HackathonDto::from).collect();
Ok(ResponseListSuccessDto {
data: dtos,
meta: result.meta,
})
}
Err(e) => {
error!("Failed to list hackathons: {}", e);
Err(ErrorDto {
status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
message: "Failed to list hackathons".to_string(),
details: None,
})
}
}
})
}
fn update_hackathon(
id: String,
payload: HackathonUpdateRequestDto,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<HackathonDto>, ErrorDto>> + Send>> {
let payload = payload;
let state = state.to_owned();
Box::pin(async move {
// Validate request
if let Err(errors) = validate_request(&payload) {
return Err(ErrorDto {
status: StatusCode::BAD_REQUEST.as_u16(),
message: "Validation failed".to_string(),
details: Some(serde_json::json!({ "validation_errors": errors.1 })),
});
}
let repo = HackathonRepository::new(&state);
// Get existing hackathon for validation
let existing = match repo.get_hackathon_by_id(id.clone()).await {
Ok(h) => h,
Err(_) => {
return Err(ErrorDto {
status: StatusCode::NOT_FOUND.as_u16(),
message: "Hackathon not found".to_string(),
details: None,
});
}
};
// Business logic validation
let start_date = payload.start_date.unwrap_or(existing.start_date);
let end_date = payload.end_date.unwrap_or(existing.end_date);
let registration_deadline = payload.registration_deadline.unwrap_or(existing.registration_deadline);
if end_date <= start_date {
return Err(ErrorDto {
status: StatusCode::BAD_REQUEST.as_u16(),
message: "End date must be after start date".to_string(),
details: None,
});
}
if registration_deadline >= start_date {
return Err(ErrorDto {
status: StatusCode::BAD_REQUEST.as_u16(),
message: "Registration deadline must be before start date".to_string(),
details: None,
});
}
match repo.update_hackathon(id, payload).await {
Ok(hackathon) => {
let dto = HackathonDto::from(hackathon);
Ok(ResponseSuccessDto { data: dto })
}
Err(e) => {
error!("Failed to update hackathon: {}", e);
Err(ErrorDto {
status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
message: "Failed to update hackathon".to_string(),
details: None,
})
}
}
})
}
fn delete_hackathon(
id: String,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<String>, ErrorDto>> + Send>> {
let state = state.to_owned();
Box::pin(async move {
let repo = HackathonRepository::new(&state);
match repo.delete_hackathon(id).await {
Ok(message) => Ok(ResponseSuccessDto { data: message }),
Err(e) => {
let error_msg = e.to_string();
if error_msg.contains("Failed to delete") {
Err(ErrorDto {
status: StatusCode::NOT_FOUND.as_u16(),
message: "Hackathon not found".to_string(),
details: None,
})
} else {
error!("Failed to delete hackathon: {}", e);
Err(ErrorDto {
status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
message: "Failed to delete hackathon".to_string(),
details: None,
})
}
}
}
})
}
fn create_hackathon_event(
hackathon_id: String,
payload: HackathonEventCreateRequestDto,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<HackathonEventDto>, ErrorDto>> + Send>> {
let payload = payload;
let state = state.to_owned();
Box::pin(async move {
// Validate request
if let Err((_, error_message)) = 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 })),
});
}
// Business logic validation
if payload.end_time <= payload.start_time {
return Err(ErrorDto {
status: StatusCode::BAD_REQUEST.as_u16(),
message: "End time must be after start time".to_string(),
details: None,
});
}
let repo = HackathonRepository::new(&state);
// Verify hackathon exists
if repo.get_hackathon_by_id(hackathon_id.clone()).await.is_err() {
return Err(ErrorDto {
status: StatusCode::NOT_FOUND.as_u16(),
message: "Hackathon not found".to_string(),
details: None,
});
}
match repo.create_hackathon_event(hackathon_id, payload).await {
Ok(event) => {
let dto = HackathonEventDto::from(event);
Ok(ResponseSuccessDto { data: dto })
}
Err(e) => {
error!("Failed to create hackathon event: {}", e);
Err(ErrorDto {
status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
message: "Failed to create hackathon event".to_string(),
details: None,
})
}
}
})
}
fn list_hackathon_events(
meta: MetaRequestDto,
hackathon_id: String,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseListSuccessDto<Vec<HackathonEventDto>>, ErrorDto>> + Send>> {
let state = state.to_owned();
Box::pin(async move {
let repo = HackathonRepository::new(&state);
match repo.list_hackathon_events(meta, hackathon_id).await {
Ok(result) => {
let dtos: Vec<HackathonEventDto> = result.data.into_iter().map(HackathonEventDto::from).collect();
Ok(ResponseListSuccessDto {
data: dtos,
meta: result.meta,
})
}
Err(e) => {
error!("Failed to list hackathon events: {}", e);
Err(ErrorDto {
status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
message: "Failed to list hackathon events".to_string(),
details: None,
})
}
}
})
}
fn update_hackathon_event(
id: String,
payload: HackathonEventUpdateRequestDto,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<HackathonEventDto>, ErrorDto>> + Send>> {
let payload = payload;
let state = state.to_owned();
Box::pin(async move {
// Validate request
if let Err((_, error_message)) = 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 })),
});
}
let repo = HackathonRepository::new(&state);
match repo.update_hackathon_event(id, payload).await {
Ok(event) => {
let dto = HackathonEventDto::from(event);
Ok(ResponseSuccessDto { data: dto })
}
Err(e) => {
let error_msg = e.to_string();
if error_msg.contains("not found") {
Err(ErrorDto {
status: StatusCode::NOT_FOUND.as_u16(),
message: "Event not found".to_string(),
details: None,
})
} else {
error!("Failed to update hackathon event: {}", e);
Err(ErrorDto {
status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
message: "Failed to update hackathon event".to_string(),
details: None,
})
}
}
}
})
}
fn delete_hackathon_event(
id: String,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<String>, ErrorDto>> + Send>> {
let state = state.to_owned();
Box::pin(async move {
let repo = HackathonRepository::new(&state);
match repo.delete_hackathon_event(id).await {
Ok(message) => Ok(ResponseSuccessDto { data: message }),
Err(e) => {
let error_msg = e.to_string();
if error_msg.contains("Failed to delete") {
Err(ErrorDto {
status: StatusCode::NOT_FOUND.as_u16(),
message: "Event not found".to_string(),
details: None,
})
} else {
error!("Failed to delete hackathon event: {}", e);
Err(ErrorDto {
status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
message: "Failed to delete hackathon event".to_string(),
details: None,
})
}
}
}
})
}
fn create_hackathon_timeline(
hackathon_id: String,
payload: HackathonTimelineCreateRequestDto,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<HackathonTimelineDto>, ErrorDto>> + Send>> {
let payload = payload;
let state = state.to_owned();
Box::pin(async move {
// Validate request
if let Err(errors) = validate_request(&payload) {
return Err(ErrorDto {
status: StatusCode::BAD_REQUEST.as_u16(),
message: "Validation failed".to_string(),
details: Some(serde_json::json!({ "validation_errors": errors.1 })),
});
}
// Business logic validation
if payload.end_date <= payload.start_date {
return Err(ErrorDto {
status: StatusCode::BAD_REQUEST.as_u16(),
message: "End date must be after start date".to_string(),
details: None,
});
}
let repo = HackathonRepository::new(&state);
// Verify hackathon exists
if repo.get_hackathon_by_id(hackathon_id.clone()).await.is_err() {
return Err(ErrorDto {
status: StatusCode::NOT_FOUND.as_u16(),
message: "Hackathon not found".to_string(),
details: None,
});
}
match repo.create_hackathon_timeline(hackathon_id, payload).await {
Ok(timeline) => {
let dto = HackathonTimelineDto::from(timeline);
Ok(ResponseSuccessDto { data: dto })
}
Err(e) => {
error!("Failed to create hackathon timeline: {}", e);
Err(ErrorDto {
status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
message: "Failed to create hackathon timeline".to_string(),
details: None,
})
}
}
})
}
fn list_hackathon_timeline(
meta: MetaRequestDto,
hackathon_id: String,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseListSuccessDto<Vec<HackathonTimelineDto>>, ErrorDto>> + Send>> {
let state = state.to_owned();
Box::pin(async move {
let repo = HackathonRepository::new(&state);
match repo.list_hackathon_timeline(meta, hackathon_id).await {
Ok(result) => {
let dtos: Vec<HackathonTimelineDto> = result.data.into_iter().map(HackathonTimelineDto::from).collect();
Ok(ResponseListSuccessDto {
data: dtos,
meta: result.meta,
})
}
Err(e) => {
error!("Failed to list hackathon timeline: {}", e);
Err(ErrorDto {
status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
message: "Failed to list hackathon timeline".to_string(),
details: None,
})
}
}
})
}
fn update_hackathon_timeline(
id: String,
payload: HackathonTimelineUpdateRequestDto,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<HackathonTimelineDto>, ErrorDto>> + Send>> {
let payload = payload;
let state = state.to_owned();
Box::pin(async move {
// Validate request
if let Err(errors) = validate_request(&payload) {
return Err(ErrorDto {
status: StatusCode::BAD_REQUEST.as_u16(),
message: "Validation failed".to_string(),
details: Some(serde_json::json!({ "validation_errors": errors.1 })),
});
}
let repo = HackathonRepository::new(&state);
match repo.update_hackathon_timeline(id, payload).await {
Ok(timeline) => {
let dto = HackathonTimelineDto::from(timeline);
Ok(ResponseSuccessDto { data: dto })
}
Err(e) => {
let error_msg = e.to_string();
if error_msg.contains("not found") {
Err(ErrorDto {
status: StatusCode::NOT_FOUND.as_u16(),
message: "Timeline not found".to_string(),
details: None,
})
} else {
error!("Failed to update hackathon timeline: {}", e);
Err(ErrorDto {
status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
message: "Failed to update hackathon timeline".to_string(),
details: None,
})
}
}
}
})
}
fn delete_hackathon_timeline(
id: String,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<String>, ErrorDto>> + Send>> {
let state = state.to_owned();
Box::pin(async move {
let repo = HackathonRepository::new(&state);
match repo.delete_hackathon_timeline(id).await {
Ok(message) => Ok(ResponseSuccessDto { data: message }),
Err(e) => {
let error_msg = e.to_string();
if error_msg.contains("Failed to delete") {
Err(ErrorDto {
status: StatusCode::NOT_FOUND.as_u16(),
message: "Timeline not found".to_string(),
details: None,
})
} else {
error!("Failed to delete hackathon timeline: {}", e);
Err(ErrorDto {
status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
message: "Failed to delete hackathon timeline".to_string(),
details: None,
})
}
}
}
})
}
fn create_hackathon_submission(
hackathon_id: String,
team_id: String,
payload: HackathonSubmissionCreateRequestDto,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<HackathonSubmissionDto>, ErrorDto>> + Send>> {
let payload = payload;
let state = state.to_owned();
Box::pin(async move {
// Validate request
if let Err(errors) = validate_request(&payload) {
return Err(ErrorDto {
status: StatusCode::BAD_REQUEST.as_u16(),
message: "Validation failed".to_string(),
details: Some(serde_json::json!({ "validation_errors": errors.1 })),
});
}
let repo = HackathonRepository::new(&state);
// Verify hackathon exists
if repo.get_hackathon_by_id(hackathon_id.clone()).await.is_err() {
return Err(ErrorDto {
status: StatusCode::NOT_FOUND.as_u16(),
message: "Hackathon not found".to_string(),
details: None,
});
}
match repo.create_hackathon_submission(hackathon_id, team_id, payload).await {
Ok(submission) => {
let dto = HackathonSubmissionDto::from(submission);
Ok(ResponseSuccessDto { data: dto })
}
Err(e) => {
error!("Failed to create hackathon submission: {}", e);
Err(ErrorDto {
status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
message: "Failed to create hackathon submission".to_string(),
details: None,
})
}
}
})
}
fn list_hackathon_submissions(
meta: MetaRequestDto,
hackathon_id: String,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseListSuccessDto<Vec<HackathonSubmissionDto>>, ErrorDto>> + Send>> {
let state = state.to_owned();
Box::pin(async move {
let repo = HackathonRepository::new(&state);
match repo.list_hackathon_submissions(meta, hackathon_id).await {
Ok(result) => {
let dtos: Vec<HackathonSubmissionDto> = result.data.into_iter().map(HackathonSubmissionDto::from).collect();
Ok(ResponseListSuccessDto {
data: dtos,
meta: result.meta,
})
}
Err(e) => {
error!("Failed to list hackathon submissions: {}", e);
Err(ErrorDto {
status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
message: "Failed to list hackathon submissions".to_string(),
details: None,
})
}
}
})
}
fn update_hackathon_submission(
id: String,
payload: HackathonSubmissionUpdateRequestDto,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<HackathonSubmissionDto>, ErrorDto>> + Send>> {
let payload = payload;
let state = state.to_owned();
Box::pin(async move {
// Validate request
if let Err(errors) = validate_request(&payload) {
return Err(ErrorDto {
status: StatusCode::BAD_REQUEST.as_u16(),
message: "Validation failed".to_string(),
details: Some(serde_json::json!({ "validation_errors": errors.1 })),
});
}
let repo = HackathonRepository::new(&state);
match repo.update_hackathon_submission(id, payload).await {
Ok(submission) => {
let dto = HackathonSubmissionDto::from(submission);
Ok(ResponseSuccessDto { data: dto })
}
Err(e) => {
let error_msg = e.to_string();
if error_msg.contains("not found") {
Err(ErrorDto {
status: StatusCode::NOT_FOUND.as_u16(),
message: "Submission not found".to_string(),
details: None,
})
} else {
error!("Failed to update hackathon submission: {}", e);
Err(ErrorDto {
status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
message: "Failed to update hackathon submission".to_string(),
details: None,
})
}
}
}
})
}
fn submit_hackathon_submission(
id: String,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<HackathonSubmissionDto>, ErrorDto>> + Send>> {
let state = state.to_owned();
Box::pin(async move {
let repo = HackathonRepository::new(&state);
match repo.submit_hackathon_submission(id).await {
Ok(submission) => {
let dto = HackathonSubmissionDto::from(submission);
Ok(ResponseSuccessDto { data: dto })
}
Err(e) => {
let error_msg = e.to_string();
if error_msg.contains("not found") {
Err(ErrorDto {
status: StatusCode::NOT_FOUND.as_u16(),
message: "Submission not found".to_string(),
details: None,
})
} else {
error!("Failed to submit hackathon submission: {}", e);
Err(ErrorDto {
status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
message: "Failed to submit hackathon submission".to_string(),
details: None,
})
}
}
}
})
}
fn delete_hackathon_submission(
id: String,
state: &AppState,
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<String>, ErrorDto>> + Send>> {
let state = state.to_owned();
Box::pin(async move {
let repo = HackathonRepository::new(&state);
match repo.delete_hackathon_submission(id).await {
Ok(message) => Ok(ResponseSuccessDto { data: message }),
Err(e) => {
let error_msg = e.to_string();
if error_msg.contains("Failed to delete") {
Err(ErrorDto {
status: StatusCode::NOT_FOUND.as_u16(),
message: "Submission not found".to_string(),
details: None,
})
} else {
error!("Failed to delete hackathon submission: {}", e);
Err(ErrorDto {
status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
message: "Failed to delete hackathon submission".to_string(),
details: None,
})
}
}
}
})
}
}
+20
View File
@@ -0,0 +1,20 @@
use axum::Router;
pub mod hackathon_controller;
pub mod hackathon_dto;
pub mod hackathon_repository;
pub mod hackathon_schema;
pub mod hackathon_service;
// Export types and functions
pub use hackathon_dto::*;
pub use hackathon_repository::HackathonRepository;
pub use hackathon_schema::*;
pub use hackathon_service::{HackathonService, HackathonServiceTrait};
// Export controller functions
pub use hackathon_controller::*;
pub fn hackathon_router() -> Router {
hackathon_controller::hackathon_routes()
}
+11
View File
@@ -0,0 +1,11 @@
use axum::Router;
pub mod hackathon;
// Export the router function from hackathon module
pub use hackathon::hackathon_router;
// Main route constructor
pub fn hackathon_protected_routes() -> Router {
Router::new().nest("/hackathons", hackathon_router())
}