diff --git a/imphnen-dimentorin/src/v1/sessions/sessions_repository.rs b/imphnen-dimentorin/src/v1/sessions/sessions_repository.rs index dbd07a5..64f716c 100644 --- a/imphnen-dimentorin/src/v1/sessions/sessions_repository.rs +++ b/imphnen-dimentorin/src/v1/sessions/sessions_repository.rs @@ -266,6 +266,86 @@ impl<'a> SessionsRepository<'a> { updated.ok_or_else(|| "Session update returned None".to_string()) } + // ============================================ + // Count Mentor Sessions + // ============================================ + pub async fn count_mentor_sessions( + &self, + mentor_id: &Thing, + status_filter: Option, + ) -> Result { + let query = if status_filter.is_some() { + "SELECT count() FROM sessions WHERE mentor_id = $mentor_id AND status = $status GROUP ALL" + } else { + "SELECT count() FROM sessions WHERE mentor_id = $mentor_id GROUP ALL" + }; + + let db = &self.state.surrealdb_ws; + let mentor_id_clone = mentor_id.clone(); + let mut result = if let Some(status_val) = status_filter { + db.query(query) + .bind(("mentor_id", mentor_id_clone)) + .bind(("status", status_val)) + .await + } else { + db.query(query) + .bind(("mentor_id", mentor_id_clone)) + .await + } + .map_err(|e| format!("Failed to count mentor sessions: {}", e))?; + + #[derive(serde::Deserialize)] + struct CountResult { + count: usize, + } + + let count_result: Option = result + .take(0) + .map_err(|e| format!("Failed to parse count: {}", e))?; + + Ok(count_result.map(|r| r.count).unwrap_or(0)) + } + + // ============================================ + // Count User Sessions + // ============================================ + pub async fn count_user_sessions( + &self, + user_id: &Thing, + status_filter: Option, + ) -> Result { + let query = if status_filter.is_some() { + "SELECT count() FROM sessions WHERE mentee_id = $user_id AND status = $status GROUP ALL" + } else { + "SELECT count() FROM sessions WHERE mentee_id = $user_id GROUP ALL" + }; + + let db = &self.state.surrealdb_ws; + let user_id_clone = user_id.clone(); + let mut result = if let Some(status_val) = status_filter { + db.query(query) + .bind(("user_id", user_id_clone)) + .bind(("status", status_val)) + .await + } else { + db.query(query) + .bind(("user_id", user_id_clone)) + .await + } + .map_err(|e| format!("Failed to count user sessions: {}", e))?; + + #[derive(serde::Deserialize)] + struct CountResult { + count: usize, + } + + let count_result: Option = result + .take(0) + .map_err(|e| format!("Failed to parse count: {}", e))?; + + Ok(count_result.map(|r| r.count).unwrap_or(0)) + } + // ============================================ // Delete Session (soft delete) // ============================================ diff --git a/imphnen-dimentorin/src/v1/sessions/sessions_service.rs b/imphnen-dimentorin/src/v1/sessions/sessions_service.rs index 1121918..92e602a 100644 --- a/imphnen-dimentorin/src/v1/sessions/sessions_service.rs +++ b/imphnen-dimentorin/src/v1/sessions/sessions_service.rs @@ -64,6 +64,13 @@ impl SessionsService { let mentor_thing = make_thing("mentors", &mentor_id); let repo = SessionsRepository::new(state); + + // Get count and sessions + let count = match repo.count_mentor_sessions(&mentor_thing, status_filter.clone()).await { + Ok(c) => c, + Err(e) => return common_response(StatusCode::INTERNAL_SERVER_ERROR, &format!("Failed to count sessions: {}", e)), + }; + match repo.query_mentor_sessions(&mentor_thing, status_filter).await { Ok(sessions) => { let session_items: Vec = sessions @@ -86,7 +93,7 @@ impl SessionsService { let response = SessionListResponseDto { sessions: session_items, - total: 0, // TODO: implement proper pagination + total: count, }; success_response(ResponseSuccessDto { data: response }) } @@ -105,6 +112,13 @@ impl SessionsService { let user_thing = make_thing("users", &user_id); let repo = SessionsRepository::new(state); + + // Get count and sessions + let count = match repo.count_user_sessions(&user_thing, status_filter.clone()).await { + Ok(c) => c, + Err(e) => return common_response(StatusCode::INTERNAL_SERVER_ERROR, &format!("Failed to count sessions: {}", e)), + }; + match repo.query_user_sessions(&user_thing, status_filter).await { Ok(sessions) => { let session_items: Vec = sessions @@ -127,7 +141,7 @@ impl SessionsService { let response = SessionListResponseDto { sessions: session_items, - total: 0, // TODO: implement proper pagination + total: count, }; success_response(ResponseSuccessDto { data: response }) } diff --git a/imphnen-hackathon/src/v1/registrations/registration_repository.rs b/imphnen-hackathon/src/v1/registrations/registration_repository.rs index 4fd6f77..82d4511 100644 --- a/imphnen-hackathon/src/v1/registrations/registration_repository.rs +++ b/imphnen-hackathon/src/v1/registrations/registration_repository.rs @@ -82,14 +82,18 @@ impl<'a> RegistrationsRepository<'a> { ) -> Result, String> { let db = &self.state.surrealdb_ws; - // Use string::join with coalesce to handle NULL team_id + // Use FETCH to retrieve related data in a single query let query = if status_filter.is_some() { r#" SELECT string::join(':', id.tb, id.id) AS id, string::join(':', hackathon_id.tb, hackathon_id.id) AS hackathon_id, + hackathon_id.name AS hackathon_name, string::join(':', user_id.tb, user_id.id) AS user_id, + user_id.fullname AS user_fullname, + user_id.email AS user_email, (IF team_id != NONE THEN string::join(':', team_id.tb, team_id.id) ELSE NONE END) AS team_id, + (IF team_id != NONE THEN team_id.name ELSE NONE END) AS team_name, status, role, registration_date, @@ -101,6 +105,7 @@ impl<'a> RegistrationsRepository<'a> { WHERE hackathon_id = $hackathon_id AND status = $status AND is_deleted = false + FETCH hackathon_id, user_id, team_id ORDER BY registration_date DESC "# } else { @@ -108,8 +113,12 @@ impl<'a> RegistrationsRepository<'a> { SELECT string::join(':', id.tb, id.id) AS id, string::join(':', hackathon_id.tb, hackathon_id.id) AS hackathon_id, + hackathon_id.name AS hackathon_name, string::join(':', user_id.tb, user_id.id) AS user_id, + user_id.fullname AS user_fullname, + user_id.email AS user_email, (IF team_id != NONE THEN string::join(':', team_id.tb, team_id.id) ELSE NONE END) AS team_id, + (IF team_id != NONE THEN team_id.name ELSE NONE END) AS team_name, status, role, registration_date, @@ -120,6 +129,7 @@ impl<'a> RegistrationsRepository<'a> { FROM hackathon_registrations WHERE hackathon_id = $hackathon_id AND is_deleted = false + FETCH hackathon_id, user_id, team_id ORDER BY registration_date DESC "# }; @@ -137,13 +147,17 @@ impl<'a> RegistrationsRepository<'a> { } .map_err(|e| format!("Failed to query hackathon registrations: {}", e))?; - // Use intermediate struct for parsing (without optional name fields) + // Use intermediate struct for parsing with all fields including related data #[derive(Debug, Serialize, Deserialize)] struct SimpleReg { id: String, hackathon_id: String, + hackathon_name: Option, user_id: String, + user_fullname: Option, + user_email: Option, team_id: Option, + team_name: Option, status: RegistrationStatus, role: ParticipantRole, registration_date: String, @@ -157,18 +171,18 @@ impl<'a> RegistrationsRepository<'a> { .take(0) .map_err(|e| format!("Failed to parse registrations: {}", e))?; - // Convert to full DTO (name fields will be None for now) + // Convert to full DTO with all fetched data let registrations = simple .into_iter() .map(|r| RegistrationListQueryDto { id: r.id, hackathon_id: r.hackathon_id, - hackathon_name: None, // TODO: Fetch separately if needed + hackathon_name: r.hackathon_name, user_id: r.user_id, - user_fullname: None, // TODO: Fetch separately if needed - user_email: None, // TODO: Fetch separately if needed + user_fullname: r.user_fullname, + user_email: r.user_email, team_id: r.team_id, - team_name: None, // TODO: Fetch separately if needed + team_name: r.team_name, status: r.status, role: r.role, registration_date: r.registration_date, @@ -187,19 +201,25 @@ impl<'a> RegistrationsRepository<'a> { // ============================================ pub async fn query_user_hackathons(&self, user_id: &Thing) -> Result, String> { let db = &self.state.surrealdb_ws; - // Use string::join with IF to handle NULL team_id + // Use FETCH to retrieve related hackathon and team data let query = r#" SELECT string::join(':', id.tb, id.id) AS registration_id, string::join(':', hackathon_id.tb, hackathon_id.id) AS hackathon_id, + hackathon_id.name AS hackathon_name, + hackathon_id.description AS hackathon_description, + hackathon_id.start_date AS start_date, + hackathon_id.end_date AS end_date, status, role, registration_date, checked_in, - (IF team_id != NONE THEN string::join(':', team_id.tb, team_id.id) ELSE NONE END) AS team_id + (IF team_id != NONE THEN string::join(':', team_id.tb, team_id.id) ELSE NONE END) AS team_id, + (IF team_id != NONE THEN team_id.name ELSE NONE END) AS team_name FROM hackathon_registrations WHERE user_id = $user_id AND is_deleted = false + FETCH hackathon_id, team_id ORDER BY registration_date DESC "#; @@ -214,33 +234,38 @@ impl<'a> RegistrationsRepository<'a> { struct SimpleUserHackathon { registration_id: String, hackathon_id: String, + hackathon_name: Option, + hackathon_description: Option, + start_date: Option, + end_date: Option, status: RegistrationStatus, role: ParticipantRole, registration_date: String, checked_in: bool, team_id: Option, + team_name: Option, } let simple: Vec = result .take(0) .map_err(|e| format!("Failed to parse user hackathons: {}", e))?; - // Convert to full DTO (name/desc fields will be None for now) + // Convert to full DTO with all fetched data let hackathons = simple .into_iter() .map(|h| UserHackathonQueryDto { registration_id: h.registration_id, hackathon_id: h.hackathon_id, - hackathon_name: None, // TODO: Fetch separately if needed - hackathon_description: None, // TODO: Fetch separately if needed - start_date: None, // TODO: Fetch separately if needed - end_date: None, // TODO: Fetch separately if needed + 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: None, // TODO: Fetch separately if needed + team_name: h.team_name, }) .collect(); @@ -253,14 +278,36 @@ impl<'a> RegistrationsRepository<'a> { pub async fn query_registration_stats(&self, hackathon_id: &Thing) -> Result { let db = &self.state.surrealdb_ws; - // Get all registrations first + // Get hackathon name first + let hackathon_query = r#" + SELECT name FROM hackathons WHERE id = $hackathon_id LIMIT 1 + "#; + + let hackathon_id_clone = hackathon_id.clone(); + let mut hackathon_result = db + .query(hackathon_query) + .bind(("hackathon_id", hackathon_id_clone.clone())) + .await + .map_err(|e| format!("Failed to fetch hackathon name: {}", e))?; + + #[derive(Debug, Serialize, Deserialize)] + struct HackathonName { + name: String, + } + + let hackathon_names: Vec = hackathon_result + .take(0) + .map_err(|e| format!("Failed to parse hackathon name: {}", e))?; + + let hackathon_name = hackathon_names.first().map(|h| h.name.clone()); + + // Get all registrations let query = r#" SELECT * FROM hackathon_registrations WHERE hackathon_id = $hackathon_id AND is_deleted = false "#; - let hackathon_id_clone = hackathon_id.clone(); let mut result = db .query(query) .bind(("hackathon_id", hackathon_id_clone)) @@ -293,7 +340,7 @@ impl<'a> RegistrationsRepository<'a> { Ok(RegistrationStatsQueryDto { hackathon_id: hackathon_id_str, - hackathon_name: None, // TODO: Fetch if needed + hackathon_name, total_registrations: total, pending, approved, diff --git a/imphnen-hackathon/src/v1/registrations/registration_service.rs b/imphnen-hackathon/src/v1/registrations/registration_service.rs index a15c763..baf992d 100644 --- a/imphnen-hackathon/src/v1/registrations/registration_service.rs +++ b/imphnen-hackathon/src/v1/registrations/registration_service.rs @@ -9,10 +9,12 @@ use surrealdb::sql::Thing; use super::{ CheckInResponseDto, RegistrationListItemDto, RegistrationListResponseDto, - RegistrationRequestDto, RegistrationResponseDto, RegistrationSchema, RegistrationStatsDto, - RegistrationStatus, RegistrationsRepository, UpdateRegistrationStatusRequestDto, + RegistrationRequestDto, RegistrationResponseDto, RegistrationSchema, + RegistrationStatsDto, RegistrationStatus, + RegistrationsRepository, UpdateRegistrationStatusRequestDto, UpdateRegistrationStatusResponseDto, UserHackathonDto, UserHackathonsResponseDto, }; +use crate::v1::hackathon::HackathonRepository; use imphnen_libs::ResourceEnum; pub struct RegistrationsService<'a> { @@ -44,7 +46,16 @@ impl<'a> RegistrationsService<'a> { let user_id = make_thing_from_enum(ResourceEnum::Users, user_email); // Check if hackathon exists - // TODO: Add hackathon existence check via hackathon repository + let hackathon_repo = HackathonRepository::new(self.state); + match hackathon_repo.get_by_id(hackathon_id).await { + Ok(None) => { + return common_response(StatusCode::NOT_FOUND, "Hackathon not found"); + } + Err(e) => { + return common_response(StatusCode::INTERNAL_SERVER_ERROR, &format!("Failed to verify hackathon: {}", e)); + } + Ok(Some(_)) => {} // Hackathon exists, continue + } // Check if user already registered match repository diff --git a/imphnen-middleware/src/payment_middleware/mod.rs b/imphnen-middleware/src/payment_middleware/mod.rs index 85cc028..8a367ec 100644 --- a/imphnen-middleware/src/payment_middleware/mod.rs +++ b/imphnen-middleware/src/payment_middleware/mod.rs @@ -1,6 +1,6 @@ use axum::{ body::Body, - http::{Request, Response}, + http::{Request, Response, StatusCode, header::HeaderMap}, }; use futures::future::BoxFuture; use imphnen_libs::AppState; @@ -52,8 +52,52 @@ where let mut inner = self.inner.clone(); let _app_state = self.app_state.clone(); Box::pin(async move { - // TODO: Implement payment validation logic here + // Payment validation logic + // Check for payment-related headers or query parameters + let headers = req.headers(); + + // Validate payment token if present + if let Some(payment_token) = headers.get("X-Payment-Token") { + if let Ok(token_str) = payment_token.to_str() { + // Basic validation: check token format + if !is_valid_payment_token(token_str) { + let error_response = Response::builder() + .status(StatusCode::PAYMENT_REQUIRED) + .body(Body::from("Invalid payment token")) + .unwrap(); + return Err(error_response); + } + } + } + + // Check if endpoint requires payment verification + let uri_path = req.uri().path(); + if requires_payment_verification(uri_path) { + if !headers.contains_key("X-Payment-Token") { + let error_response = Response::builder() + .status(StatusCode::PAYMENT_REQUIRED) + .body(Body::from("Payment required for this endpoint")) + .unwrap(); + return Err(error_response); + } + } + + // Pass through if payment validation succeeds or not required inner.call(req).await }) } +} + +/// Validate payment token format +fn is_valid_payment_token(token: &str) -> bool { + // Basic validation: token should be alphanumeric and at least 16 chars + token.len() >= 16 && token.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') +} + +/// Check if URI path requires payment verification +fn requires_payment_verification(path: &str) -> bool { + // Premium endpoints that require payment + path.contains("/premium/") || + path.contains("/paid/") || + path.contains("/subscription/") } \ No newline at end of file