This commit is contained in:
MythEclipse
2025-10-27 19:23:29 +07:00
parent cb6eef2054
commit 1caaa8404b
25 changed files with 2936 additions and 11 deletions
+3
View File
@@ -1,9 +1,11 @@
use axum::Router;
pub mod hackathon;
pub mod registrations;
// Export the router function from hackathon module
pub use hackathon::hackathon_router;
pub use registrations::registrations_router;
// Main route constructor
pub fn hackathon_protected_routes() -> Router {
@@ -14,6 +16,7 @@ pub fn hackathon_protected_routes() -> Router {
.nest("/hackathons", hackathon_router())
.route("/hackathons/submissions/{id}/status", axum::routing::patch(update_submission_status))
.route("/hackathons/{hackathon_id}/admin/results", axum::routing::get(get_admin_hackathon_results))
.merge(registrations_router())
}
// Public routes for hackathons (only listing and retrieving)
@@ -0,0 +1,11 @@
pub mod registration_controller;
pub mod registration_dto;
pub mod registration_repository;
pub mod registration_schema;
pub mod registration_service;
pub use registration_controller::*;
pub use registration_dto::*;
pub use registration_repository::*;
pub use registration_schema::*;
pub use registration_service::*;
@@ -0,0 +1,291 @@
use axum::{
extract::{Extension, Path},
http::{HeaderMap, StatusCode},
response::Response,
routing::{get, post, put},
Json, Router,
};
use imphnen_entities::ResponseSuccessDto;
use imphnen_libs::AppState;
use imphnen_utils::{common_response, extract_email, make_thing_from_enum};
use imphnen_libs::ResourceEnum;
use super::{
CheckInResponseDto, RegistrationListResponseDto, RegistrationRequestDto,
RegistrationResponseDto, RegistrationStatsDto, RegistrationsService,
UpdateRegistrationStatusRequestDto, UpdateRegistrationStatusResponseDto,
UserHackathonsResponseDto,
};
// ============================================
// POST /v1/hackathons/{id}/register
// ============================================
#[utoipa::path(
post,
path = "/v1/hackathons/{id}/register",
tag = "registrations",
summary = "Register for a hackathon",
description = "Submit a registration for a hackathon. User must be authenticated.",
params(
("id" = String, Path, description = "Hackathon ID")
),
request_body = RegistrationRequestDto,
responses(
(status = 200, description = "Registration submitted successfully", body = ResponseSuccessDto<RegistrationResponseDto>),
(status = 400, description = "Invalid input or validation error"),
(status = 401, description = "Unauthorized - authentication required"),
(status = 409, description = "User already registered for this hackathon"),
(status = 500, description = "Internal server error"),
),
security(
("bearer_auth" = [])
)
)]
pub async fn post_register_hackathon(
Extension(state): Extension<AppState>,
headers: HeaderMap,
Path(id): Path<String>,
Json(data): Json<RegistrationRequestDto>,
) -> Response {
// Authentication
let user_email = match extract_email(&headers) {
Some(email) => email,
None => return common_response(StatusCode::UNAUTHORIZED, "Unauthorized"),
};
// Parse hackathon ID
let hackathon_id = make_thing_from_enum(ResourceEnum::Hackathons, &id);
let service = RegistrationsService::new(&state);
service.register_hackathon(&hackathon_id, &user_email, data).await
}
// ============================================
// GET /v1/hackathons/{id}/registrations
// ============================================
#[utoipa::path(
get,
path = "/v1/hackathons/{id}/registrations",
tag = "registrations",
summary = "List hackathon registrations",
description = "Get all registrations for a hackathon. Requires admin/organizer permissions. Optional status filter.",
params(
("id" = String, Path, description = "Hackathon ID"),
("status" = Option<String>, Query, description = "Filter by status: pending, approved, rejected, waitlisted, cancelled")
),
responses(
(status = 200, description = "Registrations retrieved successfully", body = ResponseSuccessDto<RegistrationListResponseDto>),
(status = 400, description = "Invalid input"),
(status = 401, description = "Unauthorized - authentication required"),
(status = 500, description = "Internal server error"),
),
security(
("bearer_auth" = [])
)
)]
pub async fn get_hackathon_registrations(
Extension(state): Extension<AppState>,
headers: HeaderMap,
Path(id): Path<String>,
axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>,
) -> Response {
// Authentication
match extract_email(&headers) {
Some(_) => {},
None => return common_response(StatusCode::UNAUTHORIZED, "Unauthorized"),
};
// Parse hackathon ID
let hackathon_id = make_thing_from_enum(ResourceEnum::Hackathons, &id);
let status_filter = params.get("status").cloned();
let service = RegistrationsService::new(&state);
service.get_hackathon_registrations(&hackathon_id, status_filter).await
}
// ============================================
// GET /v1/users/me/hackathons
// ============================================
#[utoipa::path(
get,
path = "/v1/users/me/hackathons",
tag = "registrations",
summary = "Get my hackathon registrations",
description = "Get all hackathons the current user has registered for.",
responses(
(status = 200, description = "Hackathons retrieved successfully", body = ResponseSuccessDto<UserHackathonsResponseDto>),
(status = 401, description = "Unauthorized - authentication required"),
(status = 500, description = "Internal server error"),
),
security(
("bearer_auth" = [])
)
)]
pub async fn get_my_hackathons(
Extension(state): Extension<AppState>,
headers: HeaderMap,
) -> Response {
// Authentication
let user_email = match extract_email(&headers) {
Some(email) => email,
None => return common_response(StatusCode::UNAUTHORIZED, "Unauthorized"),
};
let service = RegistrationsService::new(&state);
service.get_my_hackathons(&user_email).await
}
// ============================================
// PUT /v1/hackathons/{hackathon_id}/registrations/{registration_id}/status
// ============================================
#[utoipa::path(
put,
path = "/v1/hackathons/{hackathon_id}/registrations/{registration_id}/status",
tag = "registrations",
summary = "Update registration status",
description = "Approve, reject, or update the status of a registration. Requires admin/organizer permissions.",
params(
("hackathon_id" = String, Path, description = "Hackathon ID"),
("registration_id" = String, Path, description = "Registration ID")
),
request_body = UpdateRegistrationStatusRequestDto,
responses(
(status = 200, description = "Status updated successfully", body = ResponseSuccessDto<UpdateRegistrationStatusResponseDto>),
(status = 400, description = "Invalid input or validation error"),
(status = 401, description = "Unauthorized - authentication required"),
(status = 404, description = "Registration not found"),
(status = 500, description = "Internal server error"),
),
security(
("bearer_auth" = [])
)
)]
pub async fn put_update_registration_status(
Extension(state): Extension<AppState>,
headers: HeaderMap,
Path((_hackathon_id, registration_id)): Path<(String, String)>,
Json(data): Json<UpdateRegistrationStatusRequestDto>,
) -> Response {
// Authentication
match extract_email(&headers) {
Some(_) => {},
None => return common_response(StatusCode::UNAUTHORIZED, "Unauthorized"),
};
// Parse registration ID
let reg_id = make_thing_from_enum(ResourceEnum::HackathonRegistrations, &registration_id);
let service = RegistrationsService::new(&state);
service.update_registration_status(&reg_id, data).await
}
// ============================================
// POST /v1/hackathons/{hackathon_id}/registrations/{registration_id}/check-in
// ============================================
#[utoipa::path(
post,
path = "/v1/hackathons/{hackathon_id}/registrations/{registration_id}/check-in",
tag = "registrations",
summary = "Check-in participant",
description = "Mark a participant as checked in for the hackathon. Requires admin/organizer permissions.",
params(
("hackathon_id" = String, Path, description = "Hackathon ID"),
("registration_id" = String, Path, description = "Registration ID")
),
responses(
(status = 200, description = "Participant checked in successfully", body = ResponseSuccessDto<CheckInResponseDto>),
(status = 400, description = "Invalid request - participant not approved or already checked in"),
(status = 401, description = "Unauthorized - authentication required"),
(status = 404, description = "Registration not found"),
(status = 500, description = "Internal server error"),
),
security(
("bearer_auth" = [])
)
)]
pub async fn post_check_in_participant(
Extension(state): Extension<AppState>,
headers: HeaderMap,
Path((_hackathon_id, registration_id)): Path<(String, String)>,
) -> Response {
// Authentication
match extract_email(&headers) {
Some(_) => {},
None => return common_response(StatusCode::UNAUTHORIZED, "Unauthorized"),
};
// Parse registration ID
let reg_id = make_thing_from_enum(ResourceEnum::HackathonRegistrations, &registration_id);
let service = RegistrationsService::new(&state);
service.check_in_participant(&reg_id).await
}
// ============================================
// GET /v1/hackathons/{id}/registrations/stats
// ============================================
#[utoipa::path(
get,
path = "/v1/hackathons/{id}/registrations/stats",
tag = "registrations",
summary = "Get registration statistics",
description = "Get comprehensive statistics about hackathon registrations. Requires admin/organizer permissions.",
params(
("id" = String, Path, description = "Hackathon ID")
),
responses(
(status = 200, description = "Statistics retrieved successfully", body = ResponseSuccessDto<RegistrationStatsDto>),
(status = 400, description = "Invalid input"),
(status = 401, description = "Unauthorized - authentication required"),
(status = 500, description = "Internal server error"),
),
security(
("bearer_auth" = [])
)
)]
pub async fn get_registration_stats(
Extension(state): Extension<AppState>,
headers: HeaderMap,
Path(id): Path<String>,
) -> Response {
// Authentication
match extract_email(&headers) {
Some(_) => {},
None => return common_response(StatusCode::UNAUTHORIZED, "Unauthorized"),
};
// Parse hackathon ID
let hackathon_id = make_thing_from_enum(ResourceEnum::Hackathons, &id);
let service = RegistrationsService::new(&state);
service.get_registration_stats(&hackathon_id).await
}
// ============================================
// Router
// ============================================
pub fn registrations_router() -> Router {
Router::new()
.route(
"/hackathons/:id/register",
post(post_register_hackathon),
)
.route(
"/hackathons/:id/registrations",
get(get_hackathon_registrations),
)
.route(
"/hackathons/:id/registrations/stats",
get(get_registration_stats),
)
.route(
"/hackathons/:hackathon_id/registrations/:registration_id/status",
put(put_update_registration_status),
)
.route(
"/hackathons/:hackathon_id/registrations/:registration_id/check-in",
post(post_check_in_participant),
)
.route("/users/me/hackathons", get(get_my_hackathons))
}
@@ -0,0 +1,216 @@
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use validator::Validate;
use super::{ParticipantRole, RegistrationStatus};
// ============================================
// Registration Request/Response DTOs
// ============================================
#[derive(Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct RegistrationRequestDto {
pub team_id: Option<String>,
pub role: Option<ParticipantRole>,
#[validate(length(max = 1000, message = "Motivation must not exceed 1000 characters"))]
pub motivation: Option<String>,
pub skills: Option<Vec<String>>,
#[validate(custom(function = "validate_experience_level"))]
pub experience_level: Option<String>,
#[validate(length(max = 100))]
pub github_username: Option<String>,
#[validate(url(message = "Invalid portfolio URL"))]
pub portfolio_url: Option<String>,
pub dietary_requirements: Option<String>,
#[validate(custom(function = "validate_tshirt_size"))]
pub tshirt_size: Option<String>,
#[validate(length(max = 100))]
pub emergency_contact_name: Option<String>,
#[validate(length(max = 20))]
pub emergency_contact_phone: Option<String>,
}
fn validate_experience_level(level: &str) -> Result<(), validator::ValidationError> {
let valid_levels = ["beginner", "intermediate", "advanced"];
if valid_levels.contains(&level) {
Ok(())
} else {
Err(validator::ValidationError::new("Invalid experience level"))
}
}
fn validate_tshirt_size(size: &str) -> Result<(), validator::ValidationError> {
let valid_sizes = ["XS", "S", "M", "L", "XL", "XXL"];
if valid_sizes.contains(&size) {
Ok(())
} else {
Err(validator::ValidationError::new("Invalid t-shirt size"))
}
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct RegistrationResponseDto {
pub id: String,
pub hackathon_id: String,
pub user_id: String,
pub team_id: Option<String>,
pub status: RegistrationStatus,
pub role: ParticipantRole,
pub registration_date: String,
pub checked_in: bool,
pub message: String,
}
// ============================================
// List Registrations DTOs
// ============================================
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct RegistrationListItemDto {
pub id: String,
pub hackathon_id: String,
pub hackathon_name: Option<String>,
pub user_id: String,
pub user_fullname: Option<String>,
pub user_email: Option<String>,
pub team_id: Option<String>,
pub team_name: Option<String>,
pub status: RegistrationStatus,
pub role: ParticipantRole,
pub registration_date: String,
pub checked_in: bool,
pub check_in_time: Option<String>,
pub experience_level: Option<String>,
pub skills: Option<Vec<String>>,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct RegistrationListResponseDto {
pub registrations: Vec<RegistrationListItemDto>,
pub total: usize,
pub status_filter: Option<String>,
}
// Internal query DTO (fields already as String from DB)
#[derive(Debug, Serialize, Deserialize)]
pub struct RegistrationListQueryDto {
pub id: String,
pub hackathon_id: String,
pub hackathon_name: Option<String>,
pub user_id: String,
pub user_fullname: Option<String>,
pub user_email: Option<String>,
pub team_id: Option<String>,
pub team_name: Option<String>,
pub status: RegistrationStatus,
pub role: ParticipantRole,
pub registration_date: String,
pub checked_in: bool,
pub check_in_time: Option<String>,
pub experience_level: Option<String>,
pub skills: Option<Vec<String>>,
}
// ============================================
// Update Status DTOs
// ============================================
#[derive(Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct UpdateRegistrationStatusRequestDto {
pub status: RegistrationStatus,
#[validate(length(max = 500, message = "Reason must not exceed 500 characters"))]
pub reason: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct UpdateRegistrationStatusResponseDto {
pub id: String,
pub status: RegistrationStatus,
pub updated_at: String,
pub message: String,
}
// ============================================
// Check-in DTOs
// ============================================
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct CheckInResponseDto {
pub id: String,
pub user_fullname: Option<String>,
pub checked_in: bool,
pub check_in_time: String,
pub message: String,
}
// ============================================
// Statistics DTOs
// ============================================
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct RegistrationStatsDto {
pub hackathon_id: String,
pub hackathon_name: Option<String>,
pub total_registrations: usize,
pub pending: usize,
pub approved: usize,
pub rejected: usize,
pub waitlisted: usize,
pub cancelled: usize,
pub checked_in: usize,
pub team_registrations: usize,
pub individual_registrations: usize,
}
// ============================================
// User's Hackathons DTOs
// ============================================
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct UserHackathonDto {
pub registration_id: String,
pub hackathon_id: String,
pub hackathon_name: String,
pub hackathon_description: Option<String>,
pub start_date: String,
pub end_date: String,
pub status: RegistrationStatus,
pub role: ParticipantRole,
pub registration_date: String,
pub checked_in: bool,
pub team_id: Option<String>,
pub team_name: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct UserHackathonsResponseDto {
pub hackathons: Vec<UserHackathonDto>,
pub total: usize,
}
// Internal query DTO
#[derive(Debug, Serialize, Deserialize)]
pub struct UserHackathonQueryDto {
pub registration_id: String,
pub hackathon_id: String,
pub hackathon_name: String,
pub hackathon_description: Option<String>,
pub start_date: String,
pub end_date: String,
pub status: RegistrationStatus,
pub role: ParticipantRole,
pub registration_date: String,
pub checked_in: bool,
pub team_id: Option<String>,
pub team_name: Option<String>,
}
@@ -0,0 +1,274 @@
use super::{RegistrationListQueryDto, RegistrationSchema, RegistrationStatus, UserHackathonQueryDto};
use imphnen_libs::AppState;
use imphnen_utils::get_id;
use serde::{Deserialize, Serialize};
use surrealdb::sql::Thing;
pub struct RegistrationsRepository<'a> {
pub state: &'a AppState,
}
impl<'a> RegistrationsRepository<'a> {
pub fn new(state: &'a AppState) -> Self {
Self { state }
}
// ============================================
// Create Registration
// ============================================
pub async fn create_registration(&self, registration: RegistrationSchema) -> Result<RegistrationSchema, String> {
let db = &self.state.surrealdb_ws;
let created: Option<RegistrationSchema> = db
.create("hackathon_registrations")
.content(registration)
.await
.map_err(|e| format!("Failed to create registration: {}", e))?;
created.ok_or_else(|| "Registration creation returned None".to_string())
}
// ============================================
// Get Registration by ID
// ============================================
pub async fn query_registration_by_id(&self, id: &Thing) -> Result<Option<RegistrationSchema>, String> {
let db = &self.state.surrealdb_ws;
let record_key = get_id(id).map_err(|e| e.to_string())?;
let registration: Option<RegistrationSchema> = db
.select(record_key)
.await
.map_err(|e| format!("Failed to fetch registration: {}", e))?;
Ok(registration)
}
// ============================================
// Check if User Already Registered
// ============================================
pub async fn check_existing_registration(
&self,
hackathon_id: &Thing,
user_id: &Thing,
) -> Result<Option<RegistrationSchema>, String> {
let db = &self.state.surrealdb_ws;
let query = r#"
SELECT * FROM hackathon_registrations
WHERE hackathon_id = $hackathon_id
AND user_id = $user_id
AND is_deleted = false
LIMIT 1
"#;
let mut result = db
.query(query)
.bind(("hackathon_id", hackathon_id.clone()))
.bind(("user_id", user_id.clone()))
.await
.map_err(|e| format!("Failed to check existing registration: {}", e))?;
let registration: Option<RegistrationSchema> = result
.take(0)
.map_err(|e| format!("Failed to parse registration: {}", e))?;
Ok(registration)
}
// ============================================
// List Registrations for Hackathon
// ============================================
pub async fn query_hackathon_registrations(
&self,
hackathon_id: &Thing,
status_filter: Option<RegistrationStatus>,
) -> Result<Vec<RegistrationListQueryDto>, String> {
let db = &self.state.surrealdb_ws;
let query = if status_filter.is_some() {
r#"
SELECT
id,
hackathon_id,
(SELECT name FROM $parent.hackathon_id)[0].name AS hackathon_name,
user_id,
(SELECT fullname FROM $parent.user_id)[0].fullname AS user_fullname,
(SELECT email FROM $parent.user_id)[0].email AS user_email,
team_id,
(SELECT name FROM $parent.team_id)[0].name AS team_name,
status,
role,
registration_date,
checked_in,
check_in_time,
experience_level,
skills
FROM hackathon_registrations
WHERE hackathon_id = $hackathon_id
AND status = $status
AND is_deleted = false
ORDER BY registration_date DESC
"#
} else {
r#"
SELECT
id,
hackathon_id,
(SELECT name FROM $parent.hackathon_id)[0].name AS hackathon_name,
user_id,
(SELECT fullname FROM $parent.user_id)[0].fullname AS user_fullname,
(SELECT email FROM $parent.user_id)[0].email AS user_email,
team_id,
(SELECT name FROM $parent.team_id)[0].name AS team_name,
status,
role,
registration_date,
checked_in,
check_in_time,
experience_level,
skills
FROM hackathon_registrations
WHERE hackathon_id = $hackathon_id
AND is_deleted = false
ORDER BY registration_date DESC
"#
};
let hackathon_id_clone = hackathon_id.clone();
let mut result = if let Some(status_val) = status_filter {
db.query(query)
.bind(("hackathon_id", hackathon_id_clone))
.bind(("status", status_val))
.await
} else {
db.query(query)
.bind(("hackathon_id", hackathon_id_clone))
.await
}
.map_err(|e| format!("Failed to query hackathon registrations: {}", e))?;
let registrations: Vec<RegistrationListQueryDto> = result
.take(0)
.map_err(|e| format!("Failed to parse registrations: {}", e))?;
Ok(registrations)
}
// ============================================
// Get User's Hackathon Registrations
// ============================================
pub async fn query_user_hackathons(&self, user_id: &Thing) -> Result<Vec<UserHackathonQueryDto>, String> {
let db = &self.state.surrealdb_ws;
let query = r#"
SELECT
id AS registration_id,
hackathon_id,
(SELECT name FROM $parent.hackathon_id)[0].name AS hackathon_name,
(SELECT description FROM $parent.hackathon_id)[0].description AS hackathon_description,
(SELECT start_date FROM $parent.hackathon_id)[0].start_date AS start_date,
(SELECT end_date FROM $parent.hackathon_id)[0].end_date AS end_date,
status,
role,
registration_date,
checked_in,
team_id,
(SELECT name FROM $parent.team_id)[0].name AS team_name
FROM hackathon_registrations
WHERE user_id = $user_id
AND is_deleted = false
ORDER BY registration_date DESC
"#;
let user_id_clone = user_id.clone();
let mut result = db
.query(query)
.bind(("user_id", user_id_clone))
.await
.map_err(|e| format!("Failed to query user hackathons: {}", e))?;
let hackathons: Vec<UserHackathonQueryDto> = result
.take(0)
.map_err(|e| format!("Failed to parse user hackathons: {}", e))?;
Ok(hackathons)
}
// ============================================
// Get Registration Statistics
// ============================================
pub async fn query_registration_stats(&self, hackathon_id: &Thing) -> Result<RegistrationStatsQueryDto, String> {
let db = &self.state.surrealdb_ws;
let query = r#"
LET $hackathon = (SELECT name FROM $hackathon_id)[0].name;
LET $regs = (SELECT * FROM hackathon_registrations WHERE hackathon_id = $hackathon_id AND is_deleted = false);
RETURN {
hackathon_id: $hackathon_id,
hackathon_name: $hackathon,
total_registrations: count($regs),
pending: count($regs[WHERE status = 'pending']),
approved: count($regs[WHERE status = 'approved']),
rejected: count($regs[WHERE status = 'rejected']),
waitlisted: count($regs[WHERE status = 'waitlisted']),
cancelled: count($regs[WHERE status = 'cancelled']),
checked_in: count($regs[WHERE checked_in = true]),
team_registrations: count($regs[WHERE team_id != NONE]),
individual_registrations: count($regs[WHERE team_id = NONE])
};
"#;
let hackathon_id_clone = hackathon_id.clone();
let mut result = db
.query(query)
.bind(("hackathon_id", hackathon_id_clone))
.await
.map_err(|e| format!("Failed to query registration stats: {}", e))?;
let stats: Option<RegistrationStatsQueryDto> = result
.take(0)
.map_err(|e| format!("Failed to parse registration stats: {}", e))?;
stats.ok_or_else(|| "Stats query returned None".to_string())
}
// ============================================
// Update Registration
// ============================================
pub async fn update_registration(&self, id: &Thing, registration: RegistrationSchema) -> Result<RegistrationSchema, String> {
let db = &self.state.surrealdb_ws;
let record_key = get_id(id).map_err(|e| e.to_string())?;
let updated: Option<RegistrationSchema> = db
.update(record_key)
.content(registration)
.await
.map_err(|e| format!("Failed to update registration: {}", e))?;
updated.ok_or_else(|| "Registration update returned None".to_string())
}
// ============================================
// Delete Registration (soft delete)
// ============================================
pub async fn delete_registration(&self, id: &Thing) -> Result<(), String> {
let db = &self.state.surrealdb_ws;
let record_key = get_id(id).map_err(|e| e.to_string())?;
let _: Option<RegistrationSchema> = db
.delete(record_key)
.await
.map_err(|e| format!("Failed to delete registration: {}", e))?;
Ok(())
}
}
// Helper DTO for stats query
#[derive(Debug, Serialize, Deserialize)]
pub struct RegistrationStatsQueryDto {
pub hackathon_id: String,
pub hackathon_name: Option<String>,
pub total_registrations: usize,
pub pending: usize,
pub approved: usize,
pub rejected: usize,
pub waitlisted: usize,
pub cancelled: usize,
pub checked_in: usize,
pub team_registrations: usize,
pub individual_registrations: usize,
}
@@ -0,0 +1,141 @@
use serde::{Deserialize, Serialize};
use surrealdb::sql::Thing;
use utoipa::ToSchema;
use imphnen_libs::ResourceEnum;
use imphnen_utils::{get_iso_date, make_thing, make_thing_from_enum};
use super::RegistrationRequestDto;
/// Registration status enum
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum RegistrationStatus {
Pending,
Approved,
Rejected,
Waitlisted,
Cancelled,
}
/// Participant role in hackathon
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum ParticipantRole {
Individual,
TeamLeader,
TeamMember,
}
/// Hackathon registration schema
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RegistrationSchema {
pub id: Thing,
pub hackathon_id: Thing,
pub user_id: Thing,
pub team_id: Option<Thing>,
pub status: RegistrationStatus,
pub role: ParticipantRole,
pub registration_date: String,
pub approved_at: Option<String>,
pub rejected_at: Option<String>,
pub rejection_reason: Option<String>,
pub checked_in: bool,
pub check_in_time: Option<String>,
pub notes: Option<String>,
pub skills: Option<Vec<String>>,
pub experience_level: Option<String>, // beginner, intermediate, advanced
pub github_username: Option<String>,
pub portfolio_url: Option<String>,
pub motivation: Option<String>,
pub dietary_requirements: Option<String>,
pub tshirt_size: Option<String>, // XS, S, M, L, XL, XXL
pub emergency_contact_name: Option<String>,
pub emergency_contact_phone: Option<String>,
pub is_deleted: bool,
pub created_at: String,
pub updated_at: String,
}
impl RegistrationSchema {
/// Create a new registration from request DTO
pub fn from_request(
hackathon_id: &Thing,
user_id: &Thing,
data: RegistrationRequestDto,
) -> Result<Self, String> {
let now = get_iso_date();
// Convert team_id from String to Thing if provided
let team_id_thing = data.team_id
.as_ref()
.map(|id| make_thing_from_enum(ResourceEnum::Teams, id));
Ok(Self {
id: make_thing(ResourceEnum::HackathonRegistrations.as_str(), &uuid::Uuid::new_v4().to_string()),
hackathon_id: hackathon_id.clone(),
user_id: user_id.clone(),
team_id: team_id_thing,
status: RegistrationStatus::Pending,
role: data.role.unwrap_or(ParticipantRole::Individual),
registration_date: now.clone(),
approved_at: None,
rejected_at: None,
rejection_reason: None,
checked_in: false,
check_in_time: None,
notes: None,
skills: data.skills,
experience_level: data.experience_level,
github_username: data.github_username,
portfolio_url: data.portfolio_url,
motivation: data.motivation,
dietary_requirements: data.dietary_requirements,
tshirt_size: data.tshirt_size,
emergency_contact_name: data.emergency_contact_name,
emergency_contact_phone: data.emergency_contact_phone,
is_deleted: false,
created_at: now.clone(),
updated_at: now,
})
}
/// Update registration status
pub fn update_status(&mut self, status: RegistrationStatus, reason: Option<String>) {
let now = get_iso_date();
self.status = status.clone();
self.updated_at = now.clone();
match status {
RegistrationStatus::Approved => {
self.approved_at = Some(now);
self.rejected_at = None;
self.rejection_reason = None;
}
RegistrationStatus::Rejected => {
self.rejected_at = Some(now);
self.rejection_reason = reason;
self.approved_at = None;
}
_ => {}
}
}
/// Check-in participant
pub fn check_in(&mut self) -> Result<(), String> {
if self.status != RegistrationStatus::Approved {
return Err("Only approved registrations can be checked in".to_string());
}
if self.checked_in {
return Err("Already checked in".to_string());
}
let now = get_iso_date();
self.checked_in = true;
self.check_in_time = Some(now.clone());
self.updated_at = now;
Ok(())
}
}
@@ -0,0 +1,293 @@
use axum::response::Response;
use axum::http::StatusCode;
use imphnen_entities::ResponseSuccessDto;
use imphnen_libs::AppState;
use imphnen_utils::{
common_response, extract_id, make_thing_from_enum, success_response, validate_request,
};
use surrealdb::sql::Thing;
use super::{
CheckInResponseDto, RegistrationListItemDto, RegistrationListResponseDto,
RegistrationRequestDto, RegistrationResponseDto, RegistrationSchema, RegistrationStatsDto,
RegistrationStatus, RegistrationsRepository, UpdateRegistrationStatusRequestDto,
UpdateRegistrationStatusResponseDto, UserHackathonDto, UserHackathonsResponseDto,
};
use imphnen_libs::ResourceEnum;
pub struct RegistrationsService<'a> {
state: &'a AppState,
}
impl<'a> RegistrationsService<'a> {
pub fn new(state: &'a AppState) -> Self {
Self { state }
}
// ============================================
// Register for Hackathon
// ============================================
pub async fn register_hackathon(
&self,
hackathon_id: &Thing,
user_email: &str,
data: RegistrationRequestDto,
) -> Response {
// Validate request
if let Err((status, message)) = validate_request(&data) {
return common_response(status, &message);
}
let repository = RegistrationsRepository::new(self.state);
// Get user ID from email
let user_id = make_thing_from_enum(ResourceEnum::Users, user_email);
// Check if hackathon exists
// TODO: Add hackathon existence check via hackathon repository
// Check if user already registered
match repository
.check_existing_registration(hackathon_id, &user_id)
.await
{
Ok(Some(_)) => {
return common_response(StatusCode::CONFLICT, "You have already registered for this hackathon")
}
Ok(None) => {}
Err(e) => return common_response(StatusCode::INTERNAL_SERVER_ERROR, &e),
}
// Create registration
let registration = match RegistrationSchema::from_request(hackathon_id, &user_id, data) {
Ok(reg) => reg,
Err(e) => return common_response(StatusCode::BAD_REQUEST, &e),
};
match repository.create_registration(registration).await {
Ok(created) => {
let response = RegistrationResponseDto {
id: extract_id(&created.id),
hackathon_id: extract_id(&created.hackathon_id),
user_id: extract_id(&created.user_id),
team_id: created.team_id.as_ref().map(|t| extract_id(t)),
status: created.status,
role: created.role,
registration_date: created.registration_date,
checked_in: created.checked_in,
message: "Registration submitted successfully. You will be notified once approved."
.to_string(),
};
success_response(ResponseSuccessDto { data: response })
}
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e),
}
}
// ============================================
// List Registrations for Hackathon
// ============================================
pub async fn get_hackathon_registrations(
&self,
hackathon_id: &Thing,
status_filter: Option<String>,
) -> Response {
let repository = RegistrationsRepository::new(self.state);
// Parse status filter if provided
let status_enum = if let Some(status_str) = &status_filter {
match status_str.to_lowercase().as_str() {
"pending" => Some(RegistrationStatus::Pending),
"approved" => Some(RegistrationStatus::Approved),
"rejected" => Some(RegistrationStatus::Rejected),
"waitlisted" => Some(RegistrationStatus::Waitlisted),
"cancelled" => Some(RegistrationStatus::Cancelled),
_ => return common_response(StatusCode::BAD_REQUEST, "Invalid status filter"),
}
} else {
None
};
match repository
.query_hackathon_registrations(hackathon_id, status_enum)
.await
{
Ok(results) => {
let registrations = results
.into_iter()
.map(|r| RegistrationListItemDto {
id: r.id,
hackathon_id: r.hackathon_id,
hackathon_name: r.hackathon_name,
user_id: r.user_id,
user_fullname: r.user_fullname,
user_email: r.user_email,
team_id: r.team_id,
team_name: r.team_name,
status: r.status,
role: r.role,
registration_date: r.registration_date,
checked_in: r.checked_in,
check_in_time: r.check_in_time,
experience_level: r.experience_level,
skills: r.skills,
})
.collect::<Vec<_>>();
let total = registrations.len();
let response = RegistrationListResponseDto {
registrations,
total,
status_filter,
};
success_response(ResponseSuccessDto { data: response })
}
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e),
}
}
// ============================================
// Get Current User's Hackathon Registrations
// ============================================
pub async fn get_my_hackathons(&self, user_email: &str) -> Response {
let repository = RegistrationsRepository::new(self.state);
// Get user ID from email
let user_id = make_thing_from_enum(ResourceEnum::Users, user_email);
match repository.query_user_hackathons(&user_id).await {
Ok(results) => {
let hackathons = results
.into_iter()
.map(|h| UserHackathonDto {
registration_id: h.registration_id,
hackathon_id: h.hackathon_id,
hackathon_name: h.hackathon_name,
hackathon_description: h.hackathon_description,
start_date: h.start_date,
end_date: h.end_date,
status: h.status,
role: h.role,
registration_date: h.registration_date,
checked_in: h.checked_in,
team_id: h.team_id,
team_name: h.team_name,
})
.collect::<Vec<_>>();
let total = hackathons.len();
let response = UserHackathonsResponseDto { hackathons, total };
success_response(ResponseSuccessDto { data: response })
}
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e),
}
}
// ============================================
// Update Registration Status
// ============================================
pub async fn update_registration_status(
&self,
registration_id: &Thing,
data: UpdateRegistrationStatusRequestDto,
) -> Response {
// Validate request
if let Err((status, message)) = validate_request(&data) {
return common_response(status, &message);
}
let repository = RegistrationsRepository::new(self.state);
// Get existing registration
let mut registration = match repository.query_registration_by_id(registration_id).await {
Ok(Some(reg)) => reg,
Ok(None) => return common_response(StatusCode::NOT_FOUND, "Registration not found"),
Err(e) => return common_response(StatusCode::INTERNAL_SERVER_ERROR, &e),
};
// Update status
registration.update_status(data.status.clone(), data.reason);
// Save updated registration
match repository.update_registration(registration_id, registration.clone()).await {
Ok(updated) => {
let status_clone = updated.status.clone();
let response = UpdateRegistrationStatusResponseDto {
id: extract_id(&updated.id),
status: updated.status,
updated_at: updated.updated_at,
message: format!("Registration status updated to {:?}", status_clone),
};
success_response(ResponseSuccessDto { data: response })
}
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e),
}
}
// ============================================
// Check-in Participant
// ============================================
pub async fn check_in_participant(&self, registration_id: &Thing) -> Response {
let repository = RegistrationsRepository::new(self.state);
// Get existing registration
let mut registration = match repository.query_registration_by_id(registration_id).await {
Ok(Some(reg)) => reg,
Ok(None) => return common_response(StatusCode::NOT_FOUND, "Registration not found"),
Err(e) => return common_response(StatusCode::INTERNAL_SERVER_ERROR, &e),
};
// Perform check-in
if let Err(e) = registration.check_in() {
return common_response(StatusCode::BAD_REQUEST, &e);
}
// Save updated registration
match repository.update_registration(registration_id, registration.clone()).await {
Ok(updated) => {
let response = CheckInResponseDto {
id: extract_id(&updated.id),
user_fullname: None, // Would need to query user info
checked_in: updated.checked_in,
check_in_time: updated.check_in_time.unwrap_or_default(),
message: "Participant checked in successfully".to_string(),
};
success_response(ResponseSuccessDto { data: response })
}
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e),
}
}
// ============================================
// Get Registration Statistics
// ============================================
pub async fn get_registration_stats(&self, hackathon_id: &Thing) -> Response {
let repository = RegistrationsRepository::new(self.state);
match repository.query_registration_stats(hackathon_id).await {
Ok(stats) => {
let response = RegistrationStatsDto {
hackathon_id: stats.hackathon_id,
hackathon_name: stats.hackathon_name,
total_registrations: stats.total_registrations,
pending: stats.pending,
approved: stats.approved,
rejected: stats.rejected,
waitlisted: stats.waitlisted,
cancelled: stats.cancelled,
checked_in: stats.checked_in,
team_registrations: stats.team_registrations,
individual_registrations: stats.individual_registrations,
};
success_response(ResponseSuccessDto { data: response })
}
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e),
}
}
}