feat: Add session counting methods for mentors and users; enhance registration queries with related data
This commit is contained in:
@@ -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<String>,
|
||||
) -> Result<usize, String> {
|
||||
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<CountResult> = 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<String>,
|
||||
) -> Result<usize, String> {
|
||||
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<CountResult> = 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)
|
||||
// ============================================
|
||||
|
||||
@@ -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<SessionListItemDto> = 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<SessionListItemDto> = 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 })
|
||||
}
|
||||
|
||||
@@ -82,14 +82,18 @@ impl<'a> RegistrationsRepository<'a> {
|
||||
) -> Result<Vec<RegistrationListQueryDto>, 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<String>,
|
||||
user_id: String,
|
||||
user_fullname: Option<String>,
|
||||
user_email: Option<String>,
|
||||
team_id: Option<String>,
|
||||
team_name: Option<String>,
|
||||
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<Vec<UserHackathonQueryDto>, 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<String>,
|
||||
hackathon_description: Option<String>,
|
||||
start_date: Option<String>,
|
||||
end_date: Option<String>,
|
||||
status: RegistrationStatus,
|
||||
role: ParticipantRole,
|
||||
registration_date: String,
|
||||
checked_in: bool,
|
||||
team_id: Option<String>,
|
||||
team_name: Option<String>,
|
||||
}
|
||||
|
||||
let simple: Vec<SimpleUserHackathon> = 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<RegistrationStatsQueryDto, String> {
|
||||
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<HackathonName> = 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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/")
|
||||
}
|
||||
Reference in New Issue
Block a user