feat: v0.3.0 — standardize codebase, centralize infra, merge QR into CMS
- Enforce axum best practices across all 13 workspace crates (max 200 LOC/file, no comments, no unwrap, clean architecture) - Fix domain→infrastructure dependency inversions in imphnen-iam and imphnen-dimentorin - Extract imphnen-storage (MinIO) and imphnen-email (Lettre) as standalone crates - Centralize all config in ENV struct: CDN_URL, CORS_ALLOWED_ORIGINS - Centralize SMTP through imphnen-email; remove dead HackathonConfig - Centralize database: QR crate now shares main DB pool (single DATABASE_URL) - Rename QR users table to qr_users to avoid collision with main users table - Merge imphnen-qr into imphnen-cms/src/qr (13 crates, down from 14) - Restructure imphnen-hackathon flat modules into clean architecture - Remove all stale env vars from .env.example (SurrealDB, QR_JWT, Hackathon infra) - Fix Dockerfile to include all current workspace crates - Bump all crate versions 0.2.0 → 0.3.0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
2ae43b3bcc
commit
331a4a4e88
@@ -1,117 +1,175 @@
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
use async_trait::async_trait;
|
||||
use chrono::{Utc, TimeZone};
|
||||
use imphnen_utils::errors::AppError;
|
||||
use crate::join_requests::domain::entity::*;
|
||||
use crate::join_requests::domain::repository::JoinRequestRepository;
|
||||
use crate::join_requests::domain::service::JoinRequestService;
|
||||
use async_trait::async_trait;
|
||||
use chrono::{TimeZone, Utc};
|
||||
use imphnen_utils::errors::AppError;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
fn is_team_features_closed() -> bool {
|
||||
let deadline = Utc.with_ymd_and_hms(2025, 11, 30, 16, 59, 0).unwrap();
|
||||
Utc::now() >= deadline
|
||||
let deadline = Utc
|
||||
.with_ymd_and_hms(2025, 11, 30, 16, 59, 0)
|
||||
.single()
|
||||
.expect("valid constant date");
|
||||
Utc::now() >= deadline
|
||||
}
|
||||
|
||||
pub struct JoinRequestServiceImpl {
|
||||
repo: Arc<dyn JoinRequestRepository>,
|
||||
repo: Arc<dyn JoinRequestRepository>,
|
||||
}
|
||||
|
||||
impl JoinRequestServiceImpl {
|
||||
pub fn new(repo: Arc<dyn JoinRequestRepository>) -> Self {
|
||||
Self { repo }
|
||||
}
|
||||
pub fn new(repo: Arc<dyn JoinRequestRepository>) -> Self {
|
||||
Self { repo }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl JoinRequestService for JoinRequestServiceImpl {
|
||||
async fn create_join_request(
|
||||
&self,
|
||||
team_id: Uuid,
|
||||
user_id: Uuid,
|
||||
input: CreateJoinRequestInput,
|
||||
) -> Result<JoinRequestWithDetails, AppError> {
|
||||
if is_team_features_closed() {
|
||||
return Err(AppError::BadRequestError(
|
||||
"Join requests are closed (deadline: November 30, 2025).".to_string(),
|
||||
));
|
||||
}
|
||||
if !self.repo.team_exists(team_id).await? {
|
||||
return Err(AppError::NotFoundError("Team not found".to_string()));
|
||||
}
|
||||
if self.repo.team_has_submission(team_id).await? {
|
||||
return Err(AppError::BadRequestError("Cannot request to join a team that has already submitted".to_string()));
|
||||
}
|
||||
if let Some(active_team) = self.repo.user_active_team_name(user_id).await? {
|
||||
return Err(AppError::ConflictError(format!("You are already a member of team '{}'", active_team)));
|
||||
}
|
||||
let count = self.repo.active_member_count(team_id).await?;
|
||||
if count >= 5 {
|
||||
return Err(AppError::BadRequestError("Team is already full (max 5 members)".to_string()));
|
||||
}
|
||||
if self.repo.pending_request_exists(team_id, user_id).await? {
|
||||
return Err(AppError::ConflictError("You already have a pending request for this team".to_string()));
|
||||
}
|
||||
let id = Uuid::new_v4();
|
||||
let entity = self.repo.create(id, team_id, user_id, &input.message).await?;
|
||||
let details: Vec<JoinRequestWithDetails> = self.repo.find_by_user(user_id).await?;
|
||||
details.into_iter().find(|r| r.id == entity.id)
|
||||
.ok_or_else(|| AppError::InternalServerError("Failed to retrieve created join request".to_string()))
|
||||
}
|
||||
async fn create_join_request(
|
||||
&self,
|
||||
team_id: Uuid,
|
||||
user_id: Uuid,
|
||||
input: CreateJoinRequestInput,
|
||||
) -> Result<JoinRequestWithDetails, AppError> {
|
||||
if is_team_features_closed() {
|
||||
return Err(AppError::BadRequestError(
|
||||
"Join requests are closed (deadline: November 30, 2025).".to_string(),
|
||||
));
|
||||
}
|
||||
if !self.repo.team_exists(team_id).await? {
|
||||
return Err(AppError::NotFoundError("Team not found".to_string()));
|
||||
}
|
||||
if self.repo.team_has_submission(team_id).await? {
|
||||
return Err(AppError::BadRequestError(
|
||||
"Cannot request to join a team that has already submitted".to_string(),
|
||||
));
|
||||
}
|
||||
if let Some(active_team) = self.repo.user_active_team_name(user_id).await? {
|
||||
return Err(AppError::ConflictError(format!(
|
||||
"You are already a member of team '{}'",
|
||||
active_team
|
||||
)));
|
||||
}
|
||||
let count = self.repo.active_member_count(team_id).await?;
|
||||
if count >= 5 {
|
||||
return Err(AppError::BadRequestError(
|
||||
"Team is already full (max 5 members)".to_string(),
|
||||
));
|
||||
}
|
||||
if self.repo.pending_request_exists(team_id, user_id).await? {
|
||||
return Err(AppError::ConflictError(
|
||||
"You already have a pending request for this team".to_string(),
|
||||
));
|
||||
}
|
||||
let id = Uuid::new_v4();
|
||||
let entity = self
|
||||
.repo
|
||||
.create(id, team_id, user_id, &input.message)
|
||||
.await?;
|
||||
let details: Vec<JoinRequestWithDetails> =
|
||||
self.repo.find_by_user(user_id).await?;
|
||||
details
|
||||
.into_iter()
|
||||
.find(|r| r.id == entity.id)
|
||||
.ok_or_else(|| {
|
||||
AppError::InternalServerError(
|
||||
"Failed to retrieve created join request".to_string(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_my_join_requests(&self, user_id: Uuid) -> Result<Vec<JoinRequestWithDetails>, AppError> {
|
||||
self.repo.find_by_user(user_id).await
|
||||
}
|
||||
async fn get_my_join_requests(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
) -> Result<Vec<JoinRequestWithDetails>, AppError> {
|
||||
self.repo.find_by_user(user_id).await
|
||||
}
|
||||
|
||||
async fn get_team_join_requests(
|
||||
&self,
|
||||
team_id: Uuid,
|
||||
user_id: Uuid,
|
||||
) -> Result<Vec<JoinRequestWithDetails>, AppError> {
|
||||
let leader_id = self.repo.get_team_leader_id(team_id).await?
|
||||
.ok_or_else(|| AppError::NotFoundError("Team not found".to_string()))?;
|
||||
if leader_id != user_id {
|
||||
return Err(AppError::ForbiddenError("Only the team leader can view join requests".to_string()));
|
||||
}
|
||||
self.repo.find_pending_by_team(team_id).await
|
||||
}
|
||||
async fn get_team_join_requests(
|
||||
&self,
|
||||
team_id: Uuid,
|
||||
user_id: Uuid,
|
||||
) -> Result<Vec<JoinRequestWithDetails>, AppError> {
|
||||
let leader_id = self
|
||||
.repo
|
||||
.get_team_leader_id(team_id)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::NotFoundError("Team not found".to_string()))?;
|
||||
if leader_id != user_id {
|
||||
return Err(AppError::ForbiddenError(
|
||||
"Only the team leader can view join requests".to_string(),
|
||||
));
|
||||
}
|
||||
self.repo.find_pending_by_team(team_id).await
|
||||
}
|
||||
|
||||
async fn respond_to_join_request(
|
||||
&self,
|
||||
request_id: Uuid,
|
||||
user_id: Uuid,
|
||||
accept: bool,
|
||||
) -> Result<(), AppError> {
|
||||
let request = self.repo.find_by_id(request_id).await?
|
||||
.ok_or_else(|| AppError::NotFoundError("Join request not found".to_string()))?;
|
||||
let leader_id = self.repo.get_team_leader_id(request.team_id).await?
|
||||
.ok_or_else(|| AppError::NotFoundError("Team not found".to_string()))?;
|
||||
if leader_id != user_id {
|
||||
return Err(AppError::ForbiddenError("Only the team leader can respond to join requests".to_string()));
|
||||
}
|
||||
if request.status != "pending" {
|
||||
return Err(AppError::BadRequestError("Join request is no longer pending".to_string()));
|
||||
}
|
||||
if accept {
|
||||
if is_team_features_closed() {
|
||||
return Err(AppError::BadRequestError("Team features are now closed".to_string()));
|
||||
}
|
||||
if self.repo.team_has_submission(request.team_id).await? {
|
||||
return Err(AppError::BadRequestError("Cannot accept join request after submitting a project".to_string()));
|
||||
}
|
||||
if let Some(active_team) = self.repo.user_active_team_name(request.user_id).await? {
|
||||
return Err(AppError::ConflictError(format!("User is already a member of team '{}'", active_team)));
|
||||
}
|
||||
let count = self.repo.active_member_count(request.team_id).await?;
|
||||
if count >= 5 {
|
||||
return Err(AppError::BadRequestError("Team is already full".to_string()));
|
||||
}
|
||||
self.repo.update_status(request_id, "accepted").await?;
|
||||
self.repo.add_team_member(request.team_id, request.user_id).await?;
|
||||
self.repo.reject_pending_invitations_for_user(request.user_id).await?;
|
||||
self.repo.reject_other_pending_for_user(request.user_id, request_id).await?;
|
||||
} else {
|
||||
self.repo.update_status(request_id, "rejected").await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
async fn respond_to_join_request(
|
||||
&self,
|
||||
request_id: Uuid,
|
||||
user_id: Uuid,
|
||||
accept: bool,
|
||||
) -> Result<(), AppError> {
|
||||
let request = self.repo.find_by_id(request_id).await?.ok_or_else(|| {
|
||||
AppError::NotFoundError("Join request not found".to_string())
|
||||
})?;
|
||||
let leader_id = self
|
||||
.repo
|
||||
.get_team_leader_id(request.team_id)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::NotFoundError("Team not found".to_string()))?;
|
||||
if leader_id != user_id {
|
||||
return Err(AppError::ForbiddenError(
|
||||
"Only the team leader can respond to join requests".to_string(),
|
||||
));
|
||||
}
|
||||
if request.status != "pending" {
|
||||
return Err(AppError::BadRequestError(
|
||||
"Join request is no longer pending".to_string(),
|
||||
));
|
||||
}
|
||||
if accept {
|
||||
if is_team_features_closed() {
|
||||
return Err(AppError::BadRequestError(
|
||||
"Team features are now closed".to_string(),
|
||||
));
|
||||
}
|
||||
if self.repo.team_has_submission(request.team_id).await? {
|
||||
return Err(AppError::BadRequestError(
|
||||
"Cannot accept join request after submitting a project".to_string(),
|
||||
));
|
||||
}
|
||||
if let Some(active_team) =
|
||||
self.repo.user_active_team_name(request.user_id).await?
|
||||
{
|
||||
return Err(AppError::ConflictError(format!(
|
||||
"User is already a member of team '{}'",
|
||||
active_team
|
||||
)));
|
||||
}
|
||||
let count = self.repo.active_member_count(request.team_id).await?;
|
||||
if count >= 5 {
|
||||
return Err(AppError::BadRequestError(
|
||||
"Team is already full".to_string(),
|
||||
));
|
||||
}
|
||||
self.repo.update_status(request_id, "accepted").await?;
|
||||
self
|
||||
.repo
|
||||
.add_team_member(request.team_id, request.user_id)
|
||||
.await?;
|
||||
self
|
||||
.repo
|
||||
.reject_pending_invitations_for_user(request.user_id)
|
||||
.await?;
|
||||
self
|
||||
.repo
|
||||
.reject_other_pending_for_user(request.user_id, request_id)
|
||||
.await?;
|
||||
} else {
|
||||
self.repo.update_status(request_id, "rejected").await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
use uuid::Uuid;
|
||||
use chrono::{DateTime, Utc};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct JoinRequestEntity {
|
||||
pub id: Uuid,
|
||||
pub team_id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub message: String,
|
||||
pub status: String,
|
||||
pub created_at: Option<DateTime<Utc>>,
|
||||
pub id: Uuid,
|
||||
pub team_id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub message: String,
|
||||
pub status: String,
|
||||
pub created_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct JoinRequestWithDetails {
|
||||
pub id: Uuid,
|
||||
pub team_id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub user_fullname: String,
|
||||
pub user_email: String,
|
||||
pub user_avatar: Option<String>,
|
||||
pub message: String,
|
||||
pub status: String,
|
||||
pub created_at: Option<DateTime<Utc>>,
|
||||
pub id: Uuid,
|
||||
pub team_id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub user_fullname: String,
|
||||
pub user_email: String,
|
||||
pub user_avatar: Option<String>,
|
||||
pub message: String,
|
||||
pub status: String,
|
||||
pub created_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct CreateJoinRequestInput {
|
||||
pub message: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
@@ -1,43 +1,73 @@
|
||||
use async_trait::async_trait;
|
||||
use uuid::Uuid;
|
||||
use imphnen_utils::errors::AppError;
|
||||
use super::entity::*;
|
||||
use async_trait::async_trait;
|
||||
use imphnen_utils::errors::AppError;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[async_trait]
|
||||
pub trait JoinRequestRepository: Send + Sync {
|
||||
async fn create(
|
||||
&self,
|
||||
id: Uuid,
|
||||
team_id: Uuid,
|
||||
user_id: Uuid,
|
||||
message: &str,
|
||||
) -> Result<JoinRequestEntity, AppError>;
|
||||
async fn create(
|
||||
&self,
|
||||
id: Uuid,
|
||||
team_id: Uuid,
|
||||
user_id: Uuid,
|
||||
message: &str,
|
||||
) -> Result<JoinRequestEntity, AppError>;
|
||||
|
||||
async fn find_by_id(&self, id: Uuid) -> Result<Option<JoinRequestEntity>, AppError>;
|
||||
async fn find_by_id(
|
||||
&self,
|
||||
id: Uuid,
|
||||
) -> Result<Option<JoinRequestEntity>, AppError>;
|
||||
|
||||
async fn find_by_user(&self, user_id: Uuid) -> Result<Vec<JoinRequestWithDetails>, AppError>;
|
||||
async fn find_by_user(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
) -> Result<Vec<JoinRequestWithDetails>, AppError>;
|
||||
|
||||
async fn find_pending_by_team(&self, team_id: Uuid) -> Result<Vec<JoinRequestWithDetails>, AppError>;
|
||||
async fn find_pending_by_team(
|
||||
&self,
|
||||
team_id: Uuid,
|
||||
) -> Result<Vec<JoinRequestWithDetails>, AppError>;
|
||||
|
||||
async fn update_status(&self, id: Uuid, status: &str) -> Result<(), AppError>;
|
||||
async fn update_status(&self, id: Uuid, status: &str) -> Result<(), AppError>;
|
||||
|
||||
async fn add_team_member(&self, team_id: Uuid, user_id: Uuid) -> Result<(), AppError>;
|
||||
async fn add_team_member(
|
||||
&self,
|
||||
team_id: Uuid,
|
||||
user_id: Uuid,
|
||||
) -> Result<(), AppError>;
|
||||
|
||||
async fn reject_pending_invitations_for_user(&self, user_id: Uuid) -> Result<(), AppError>;
|
||||
async fn reject_pending_invitations_for_user(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
) -> Result<(), AppError>;
|
||||
|
||||
async fn reject_other_pending_for_user(&self, user_id: Uuid, except_id: Uuid) -> Result<(), AppError>;
|
||||
async fn reject_other_pending_for_user(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
except_id: Uuid,
|
||||
) -> Result<(), AppError>;
|
||||
|
||||
async fn get_team_leader_id(&self, team_id: Uuid) -> Result<Option<Uuid>, AppError>;
|
||||
async fn get_team_leader_id(
|
||||
&self,
|
||||
team_id: Uuid,
|
||||
) -> Result<Option<Uuid>, AppError>;
|
||||
|
||||
async fn get_user_email(&self, user_id: Uuid) -> Result<Option<String>, AppError>;
|
||||
async fn get_user_email(&self, user_id: Uuid) -> Result<Option<String>, AppError>;
|
||||
|
||||
async fn team_exists(&self, team_id: Uuid) -> Result<bool, AppError>;
|
||||
async fn team_exists(&self, team_id: Uuid) -> Result<bool, AppError>;
|
||||
|
||||
async fn team_has_submission(&self, team_id: Uuid) -> Result<bool, AppError>;
|
||||
async fn team_has_submission(&self, team_id: Uuid) -> Result<bool, AppError>;
|
||||
|
||||
async fn user_active_team_name(&self, user_id: Uuid) -> Result<Option<String>, AppError>;
|
||||
async fn user_active_team_name(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
) -> Result<Option<String>, AppError>;
|
||||
|
||||
async fn active_member_count(&self, team_id: Uuid) -> Result<i64, AppError>;
|
||||
async fn active_member_count(&self, team_id: Uuid) -> Result<i64, AppError>;
|
||||
|
||||
async fn pending_request_exists(&self, team_id: Uuid, user_id: Uuid) -> Result<bool, AppError>;
|
||||
async fn pending_request_exists(
|
||||
&self,
|
||||
team_id: Uuid,
|
||||
user_id: Uuid,
|
||||
) -> Result<bool, AppError>;
|
||||
}
|
||||
|
||||
@@ -1,29 +1,32 @@
|
||||
use async_trait::async_trait;
|
||||
use uuid::Uuid;
|
||||
use imphnen_utils::errors::AppError;
|
||||
use super::entity::*;
|
||||
use async_trait::async_trait;
|
||||
use imphnen_utils::errors::AppError;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[async_trait]
|
||||
pub trait JoinRequestService: Send + Sync {
|
||||
async fn create_join_request(
|
||||
&self,
|
||||
team_id: Uuid,
|
||||
user_id: Uuid,
|
||||
input: CreateJoinRequestInput,
|
||||
) -> Result<JoinRequestWithDetails, AppError>;
|
||||
async fn create_join_request(
|
||||
&self,
|
||||
team_id: Uuid,
|
||||
user_id: Uuid,
|
||||
input: CreateJoinRequestInput,
|
||||
) -> Result<JoinRequestWithDetails, AppError>;
|
||||
|
||||
async fn get_my_join_requests(&self, user_id: Uuid) -> Result<Vec<JoinRequestWithDetails>, AppError>;
|
||||
async fn get_my_join_requests(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
) -> Result<Vec<JoinRequestWithDetails>, AppError>;
|
||||
|
||||
async fn get_team_join_requests(
|
||||
&self,
|
||||
team_id: Uuid,
|
||||
user_id: Uuid,
|
||||
) -> Result<Vec<JoinRequestWithDetails>, AppError>;
|
||||
async fn get_team_join_requests(
|
||||
&self,
|
||||
team_id: Uuid,
|
||||
user_id: Uuid,
|
||||
) -> Result<Vec<JoinRequestWithDetails>, AppError>;
|
||||
|
||||
async fn respond_to_join_request(
|
||||
&self,
|
||||
request_id: Uuid,
|
||||
user_id: Uuid,
|
||||
accept: bool,
|
||||
) -> Result<(), AppError>;
|
||||
async fn respond_to_join_request(
|
||||
&self,
|
||||
request_id: Uuid,
|
||||
user_id: Uuid,
|
||||
accept: bool,
|
||||
) -> Result<(), AppError>;
|
||||
}
|
||||
|
||||
@@ -1,50 +1,50 @@
|
||||
use crate::join_requests::domain::entity::*;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
use chrono::{DateTime, Utc};
|
||||
use crate::join_requests::domain::entity::*;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct JoinRequestResponse {
|
||||
pub id: Uuid,
|
||||
pub team_id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub user_fullname: String,
|
||||
pub user_email: String,
|
||||
pub user_avatar: Option<String>,
|
||||
pub message: String,
|
||||
pub status: String,
|
||||
pub created_at: Option<DateTime<Utc>>,
|
||||
pub id: Uuid,
|
||||
pub team_id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub user_fullname: String,
|
||||
pub user_email: String,
|
||||
pub user_avatar: Option<String>,
|
||||
pub message: String,
|
||||
pub status: String,
|
||||
pub created_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl From<JoinRequestWithDetails> for JoinRequestResponse {
|
||||
fn from(e: JoinRequestWithDetails) -> Self {
|
||||
Self {
|
||||
id: e.id,
|
||||
team_id: e.team_id,
|
||||
user_id: e.user_id,
|
||||
user_fullname: e.user_fullname,
|
||||
user_email: e.user_email,
|
||||
user_avatar: e.user_avatar,
|
||||
message: e.message,
|
||||
status: e.status,
|
||||
created_at: e.created_at,
|
||||
}
|
||||
}
|
||||
fn from(e: JoinRequestWithDetails) -> Self {
|
||||
Self {
|
||||
id: e.id,
|
||||
team_id: e.team_id,
|
||||
user_id: e.user_id,
|
||||
user_fullname: e.user_fullname,
|
||||
user_email: e.user_email,
|
||||
user_avatar: e.user_avatar,
|
||||
message: e.message,
|
||||
status: e.status,
|
||||
created_at: e.created_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct CreateJoinRequestRequest {
|
||||
pub message: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl From<CreateJoinRequestRequest> for CreateJoinRequestInput {
|
||||
fn from(r: CreateJoinRequestRequest) -> Self {
|
||||
Self { message: r.message }
|
||||
}
|
||||
fn from(r: CreateJoinRequestRequest) -> Self {
|
||||
Self { message: r.message }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct RespondToJoinRequestRequest {
|
||||
pub accept: bool,
|
||||
pub accept: bool,
|
||||
}
|
||||
|
||||
@@ -1,47 +1,62 @@
|
||||
use super::dto::*;
|
||||
use crate::join_requests::domain::service::JoinRequestService;
|
||||
use crate::middleware::hackathon_auth::HackathonAuthUser;
|
||||
use axum::{Extension, Json, extract::Path, response::IntoResponse};
|
||||
use imphnen_utils::{
|
||||
errors::AppError,
|
||||
response_format::{ApiMessage, ApiSuccess},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
use imphnen_utils::{errors::AppError, response_format::{ApiSuccess, ApiMessage}};
|
||||
use crate::middleware::hackathon_auth::HackathonAuthUser;
|
||||
use crate::join_requests::domain::service::JoinRequestService;
|
||||
use super::dto::*;
|
||||
|
||||
pub async fn create_join_request_handler(
|
||||
Extension(service): Extension<Arc<dyn JoinRequestService>>,
|
||||
Extension(auth): Extension<HackathonAuthUser>,
|
||||
Path(team_id): Path<Uuid>,
|
||||
Json(body): Json<CreateJoinRequestRequest>,
|
||||
Extension(service): Extension<Arc<dyn JoinRequestService>>,
|
||||
Extension(auth): Extension<HackathonAuthUser>,
|
||||
Path(team_id): Path<Uuid>,
|
||||
Json(body): Json<CreateJoinRequestRequest>,
|
||||
) -> Result<axum::response::Response, AppError> {
|
||||
let request = service.create_join_request(team_id, auth.user_id, body.into()).await?;
|
||||
Ok(ApiSuccess(JoinRequestResponse::from(request)).into_response())
|
||||
let request = service
|
||||
.create_join_request(team_id, auth.user_id, body.into())
|
||||
.await?;
|
||||
Ok(ApiSuccess(JoinRequestResponse::from(request)).into_response())
|
||||
}
|
||||
|
||||
pub async fn get_my_join_requests_handler(
|
||||
Extension(service): Extension<Arc<dyn JoinRequestService>>,
|
||||
Extension(auth): Extension<HackathonAuthUser>,
|
||||
Extension(service): Extension<Arc<dyn JoinRequestService>>,
|
||||
Extension(auth): Extension<HackathonAuthUser>,
|
||||
) -> Result<axum::response::Response, AppError> {
|
||||
let list = service.get_my_join_requests(auth.user_id).await?;
|
||||
let response: Vec<JoinRequestResponse> = list.into_iter().map(JoinRequestResponse::from).collect();
|
||||
Ok(ApiSuccess(response).into_response())
|
||||
let list = service.get_my_join_requests(auth.user_id).await?;
|
||||
let response: Vec<JoinRequestResponse> =
|
||||
list.into_iter().map(JoinRequestResponse::from).collect();
|
||||
Ok(ApiSuccess(response).into_response())
|
||||
}
|
||||
|
||||
pub async fn get_team_join_requests_handler(
|
||||
Extension(service): Extension<Arc<dyn JoinRequestService>>,
|
||||
Extension(auth): Extension<HackathonAuthUser>,
|
||||
Path(team_id): Path<Uuid>,
|
||||
Extension(service): Extension<Arc<dyn JoinRequestService>>,
|
||||
Extension(auth): Extension<HackathonAuthUser>,
|
||||
Path(team_id): Path<Uuid>,
|
||||
) -> Result<axum::response::Response, AppError> {
|
||||
let list = service.get_team_join_requests(team_id, auth.user_id).await?;
|
||||
let response: Vec<JoinRequestResponse> = list.into_iter().map(JoinRequestResponse::from).collect();
|
||||
Ok(ApiSuccess(response).into_response())
|
||||
let list = service
|
||||
.get_team_join_requests(team_id, auth.user_id)
|
||||
.await?;
|
||||
let response: Vec<JoinRequestResponse> =
|
||||
list.into_iter().map(JoinRequestResponse::from).collect();
|
||||
Ok(ApiSuccess(response).into_response())
|
||||
}
|
||||
|
||||
pub async fn respond_to_join_request_handler(
|
||||
Extension(service): Extension<Arc<dyn JoinRequestService>>,
|
||||
Extension(auth): Extension<HackathonAuthUser>,
|
||||
Path(request_id): Path<Uuid>,
|
||||
Json(body): Json<RespondToJoinRequestRequest>,
|
||||
Extension(service): Extension<Arc<dyn JoinRequestService>>,
|
||||
Extension(auth): Extension<HackathonAuthUser>,
|
||||
Path(request_id): Path<Uuid>,
|
||||
Json(body): Json<RespondToJoinRequestRequest>,
|
||||
) -> Result<axum::response::Response, AppError> {
|
||||
service.respond_to_join_request(request_id, auth.user_id, body.accept).await?;
|
||||
let msg = if body.accept { "Join request accepted" } else { "Join request rejected" };
|
||||
Ok(ApiMessage::ok(msg).into_response())
|
||||
service
|
||||
.respond_to_join_request(request_id, auth.user_id, body.accept)
|
||||
.await?;
|
||||
let msg = if body.accept {
|
||||
"Join request accepted"
|
||||
} else {
|
||||
"Join request rejected"
|
||||
};
|
||||
Ok(ApiMessage::ok(msg).into_response())
|
||||
}
|
||||
|
||||
@@ -1,22 +1,35 @@
|
||||
use axum::{middleware::from_fn, routing::{get, post}, Extension, Router};
|
||||
use sqlx::PgPool;
|
||||
use std::sync::Arc;
|
||||
use super::handlers::*;
|
||||
use crate::join_requests::application::join_request_service::JoinRequestServiceImpl;
|
||||
use crate::join_requests::domain::service::JoinRequestService;
|
||||
use crate::join_requests::infrastructure::persistence::PostgresJoinRequestRepository;
|
||||
use crate::middleware::hackathon_auth::hackathon_auth_middleware;
|
||||
use super::handlers::*;
|
||||
use axum::{
|
||||
Extension, Router,
|
||||
middleware::from_fn,
|
||||
routing::{get, post},
|
||||
};
|
||||
use sqlx::PgPool;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn build_join_request_routes(pool: Arc<PgPool>) -> Router {
|
||||
let service: Arc<dyn JoinRequestService> = Arc::new(JoinRequestServiceImpl::new(
|
||||
Arc::new(PostgresJoinRequestRepository::new(pool.clone())),
|
||||
));
|
||||
Router::new()
|
||||
.route("/join-requests/teams/:team_id", post(create_join_request_handler))
|
||||
.route("/join-requests/my", get(get_my_join_requests_handler))
|
||||
.route("/join-requests/teams/:team_id/pending", get(get_team_join_requests_handler))
|
||||
.route("/join-requests/:request_id/respond", post(respond_to_join_request_handler))
|
||||
.layer(Extension(service))
|
||||
.layer(Extension(pool))
|
||||
.layer(from_fn(hackathon_auth_middleware))
|
||||
let service: Arc<dyn JoinRequestService> = Arc::new(JoinRequestServiceImpl::new(
|
||||
Arc::new(PostgresJoinRequestRepository::new(pool.clone())),
|
||||
));
|
||||
Router::new()
|
||||
.route(
|
||||
"/join-requests/teams/:team_id",
|
||||
post(create_join_request_handler),
|
||||
)
|
||||
.route("/join-requests/my", get(get_my_join_requests_handler))
|
||||
.route(
|
||||
"/join-requests/teams/:team_id/pending",
|
||||
get(get_team_join_requests_handler),
|
||||
)
|
||||
.route(
|
||||
"/join-requests/:request_id/respond",
|
||||
post(respond_to_join_request_handler),
|
||||
)
|
||||
.layer(Extension(service))
|
||||
.layer(Extension(pool))
|
||||
.layer(from_fn(hackathon_auth_middleware))
|
||||
}
|
||||
|
||||
+171
-114
@@ -1,173 +1,230 @@
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
use chrono::{DateTime, Utc};
|
||||
use async_trait::async_trait;
|
||||
use sqlx::{PgPool, FromRow};
|
||||
use imphnen_utils::errors::AppError;
|
||||
use crate::join_requests::domain::entity::*;
|
||||
use crate::join_requests::domain::repository::JoinRequestRepository;
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use imphnen_utils::errors::AppError;
|
||||
use sqlx::{FromRow, PgPool};
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(FromRow)]
|
||||
struct JoinRequestRow {
|
||||
id: Uuid,
|
||||
team_id: Uuid,
|
||||
user_id: Uuid,
|
||||
message: String,
|
||||
status: String,
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
id: Uuid,
|
||||
team_id: Uuid,
|
||||
user_id: Uuid,
|
||||
message: String,
|
||||
status: String,
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl From<JoinRequestRow> for JoinRequestEntity {
|
||||
fn from(r: JoinRequestRow) -> Self {
|
||||
Self {
|
||||
id: r.id,
|
||||
team_id: r.team_id,
|
||||
user_id: r.user_id,
|
||||
message: r.message,
|
||||
status: r.status,
|
||||
created_at: r.created_at,
|
||||
}
|
||||
}
|
||||
fn from(r: JoinRequestRow) -> Self {
|
||||
Self {
|
||||
id: r.id,
|
||||
team_id: r.team_id,
|
||||
user_id: r.user_id,
|
||||
message: r.message,
|
||||
status: r.status,
|
||||
created_at: r.created_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromRow)]
|
||||
struct JoinRequestDetailsRow {
|
||||
id: Uuid,
|
||||
team_id: Uuid,
|
||||
user_id: Uuid,
|
||||
user_fullname: String,
|
||||
user_email: String,
|
||||
user_avatar: Option<String>,
|
||||
message: String,
|
||||
status: String,
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
id: Uuid,
|
||||
team_id: Uuid,
|
||||
user_id: Uuid,
|
||||
user_fullname: String,
|
||||
user_email: String,
|
||||
user_avatar: Option<String>,
|
||||
message: String,
|
||||
status: String,
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl From<JoinRequestDetailsRow> for JoinRequestWithDetails {
|
||||
fn from(r: JoinRequestDetailsRow) -> Self {
|
||||
Self {
|
||||
id: r.id,
|
||||
team_id: r.team_id,
|
||||
user_id: r.user_id,
|
||||
user_fullname: r.user_fullname,
|
||||
user_email: r.user_email,
|
||||
user_avatar: r.user_avatar,
|
||||
message: r.message,
|
||||
status: r.status,
|
||||
created_at: r.created_at,
|
||||
}
|
||||
}
|
||||
fn from(r: JoinRequestDetailsRow) -> Self {
|
||||
Self {
|
||||
id: r.id,
|
||||
team_id: r.team_id,
|
||||
user_id: r.user_id,
|
||||
user_fullname: r.user_fullname,
|
||||
user_email: r.user_email,
|
||||
user_avatar: r.user_avatar,
|
||||
message: r.message,
|
||||
status: r.status,
|
||||
created_at: r.created_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PostgresJoinRequestRepository {
|
||||
pool: Arc<PgPool>,
|
||||
pool: Arc<PgPool>,
|
||||
}
|
||||
|
||||
impl PostgresJoinRequestRepository {
|
||||
pub fn new(pool: Arc<PgPool>) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
pub fn new(pool: Arc<PgPool>) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl JoinRequestRepository for PostgresJoinRequestRepository {
|
||||
async fn create(&self, id: Uuid, team_id: Uuid, user_id: Uuid, message: &str) -> Result<JoinRequestEntity, AppError> {
|
||||
let row: JoinRequestRow = sqlx::query_as(
|
||||
async fn create(
|
||||
&self,
|
||||
id: Uuid,
|
||||
team_id: Uuid,
|
||||
user_id: Uuid,
|
||||
message: &str,
|
||||
) -> Result<JoinRequestEntity, AppError> {
|
||||
let row: JoinRequestRow = sqlx::query_as(
|
||||
"INSERT INTO hackathon_team_join_requests (id, team_id, user_id, message, status, created_at) VALUES ($1, $2, $3, $4, 'pending', NOW()) RETURNING id, team_id, user_id, message, status, created_at"
|
||||
)
|
||||
.bind(id).bind(team_id).bind(user_id).bind(message)
|
||||
.fetch_one(self.pool.as_ref()).await.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(row.into())
|
||||
}
|
||||
Ok(row.into())
|
||||
}
|
||||
|
||||
async fn find_by_id(&self, id: Uuid) -> Result<Option<JoinRequestEntity>, AppError> {
|
||||
let row: Option<JoinRequestRow> = sqlx::query_as(
|
||||
async fn find_by_id(
|
||||
&self,
|
||||
id: Uuid,
|
||||
) -> Result<Option<JoinRequestEntity>, AppError> {
|
||||
let row: Option<JoinRequestRow> = sqlx::query_as(
|
||||
"SELECT id, team_id, user_id, message, status, created_at FROM hackathon_team_join_requests WHERE id = $1"
|
||||
)
|
||||
.bind(id).fetch_optional(self.pool.as_ref()).await.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(row.map(Into::into))
|
||||
}
|
||||
Ok(row.map(Into::into))
|
||||
}
|
||||
|
||||
async fn find_by_user(&self, user_id: Uuid) -> Result<Vec<JoinRequestWithDetails>, AppError> {
|
||||
let rows: Vec<JoinRequestDetailsRow> = sqlx::query_as(
|
||||
async fn find_by_user(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
) -> Result<Vec<JoinRequestWithDetails>, AppError> {
|
||||
let rows: Vec<JoinRequestDetailsRow> = sqlx::query_as(
|
||||
"SELECT r.id, r.team_id, r.user_id, u.fullname AS user_fullname, u.email AS user_email, u.avatar AS user_avatar, r.message, r.status, r.created_at FROM hackathon_team_join_requests r JOIN hackathon_users u ON u.id = r.user_id WHERE r.user_id = $1 ORDER BY r.created_at DESC"
|
||||
)
|
||||
.bind(user_id).fetch_all(self.pool.as_ref()).await.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(rows.into_iter().map(Into::into).collect())
|
||||
}
|
||||
Ok(rows.into_iter().map(Into::into).collect())
|
||||
}
|
||||
|
||||
async fn find_pending_by_team(&self, team_id: Uuid) -> Result<Vec<JoinRequestWithDetails>, AppError> {
|
||||
let rows: Vec<JoinRequestDetailsRow> = sqlx::query_as(
|
||||
async fn find_pending_by_team(
|
||||
&self,
|
||||
team_id: Uuid,
|
||||
) -> Result<Vec<JoinRequestWithDetails>, AppError> {
|
||||
let rows: Vec<JoinRequestDetailsRow> = sqlx::query_as(
|
||||
"SELECT r.id, r.team_id, r.user_id, u.fullname AS user_fullname, u.email AS user_email, u.avatar AS user_avatar, r.message, r.status, r.created_at FROM hackathon_team_join_requests r JOIN hackathon_users u ON u.id = r.user_id WHERE r.team_id = $1 AND r.status = 'pending' ORDER BY r.created_at ASC"
|
||||
)
|
||||
.bind(team_id).fetch_all(self.pool.as_ref()).await.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(rows.into_iter().map(Into::into).collect())
|
||||
}
|
||||
Ok(rows.into_iter().map(Into::into).collect())
|
||||
}
|
||||
|
||||
async fn update_status(&self, id: Uuid, status: &str) -> Result<(), AppError> {
|
||||
sqlx::query("UPDATE hackathon_team_join_requests SET status = $1 WHERE id = $2")
|
||||
.bind(status).bind(id)
|
||||
.execute(self.pool.as_ref()).await.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
async fn update_status(&self, id: Uuid, status: &str) -> Result<(), AppError> {
|
||||
sqlx::query("UPDATE hackathon_team_join_requests SET status = $1 WHERE id = $2")
|
||||
.bind(status)
|
||||
.bind(id)
|
||||
.execute(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn add_team_member(&self, team_id: Uuid, user_id: Uuid) -> Result<(), AppError> {
|
||||
sqlx::query("INSERT INTO hackathon_team_members (id, team_id, user_id, role, status, joined_at) VALUES ($1, $2, $3, 'member', 'active', NOW())")
|
||||
async fn add_team_member(
|
||||
&self,
|
||||
team_id: Uuid,
|
||||
user_id: Uuid,
|
||||
) -> Result<(), AppError> {
|
||||
sqlx::query("INSERT INTO hackathon_team_members (id, team_id, user_id, role, status, joined_at) VALUES ($1, $2, $3, 'member', 'active', NOW())")
|
||||
.bind(Uuid::new_v4()).bind(team_id).bind(user_id)
|
||||
.execute(self.pool.as_ref()).await.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn reject_pending_invitations_for_user(&self, user_id: Uuid) -> Result<(), AppError> {
|
||||
let email: Option<String> = sqlx::query_scalar("SELECT email FROM hackathon_users WHERE id = $1")
|
||||
.bind(user_id).fetch_optional(self.pool.as_ref()).await.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
if let Some(email) = email {
|
||||
sqlx::query("UPDATE hackathon_team_invitations SET status = 'rejected' WHERE invitee_email = $1 AND status = 'pending'")
|
||||
async fn reject_pending_invitations_for_user(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
) -> Result<(), AppError> {
|
||||
let email: Option<String> =
|
||||
sqlx::query_scalar("SELECT email FROM hackathon_users WHERE id = $1")
|
||||
.bind(user_id)
|
||||
.fetch_optional(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
if let Some(email) = email {
|
||||
sqlx::query("UPDATE hackathon_team_invitations SET status = 'rejected' WHERE invitee_email = $1 AND status = 'pending'")
|
||||
.bind(email)
|
||||
.execute(self.pool.as_ref()).await.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn reject_other_pending_for_user(&self, user_id: Uuid, except_id: Uuid) -> Result<(), AppError> {
|
||||
sqlx::query("UPDATE hackathon_team_join_requests SET status = 'rejected' WHERE user_id = $1 AND status = 'pending' AND id != $2")
|
||||
async fn reject_other_pending_for_user(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
except_id: Uuid,
|
||||
) -> Result<(), AppError> {
|
||||
sqlx::query("UPDATE hackathon_team_join_requests SET status = 'rejected' WHERE user_id = $1 AND status = 'pending' AND id != $2")
|
||||
.bind(user_id).bind(except_id)
|
||||
.execute(self.pool.as_ref()).await.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_team_leader_id(&self, team_id: Uuid) -> Result<Option<Uuid>, AppError> {
|
||||
sqlx::query_scalar("SELECT leader_id FROM hackathon_teams WHERE id = $1")
|
||||
.bind(team_id).fetch_optional(self.pool.as_ref()).await.map_err(|e| AppError::InternalServerError(e.to_string()))
|
||||
}
|
||||
async fn get_team_leader_id(
|
||||
&self,
|
||||
team_id: Uuid,
|
||||
) -> Result<Option<Uuid>, AppError> {
|
||||
sqlx::query_scalar("SELECT leader_id FROM hackathon_teams WHERE id = $1")
|
||||
.bind(team_id)
|
||||
.fetch_optional(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))
|
||||
}
|
||||
|
||||
async fn get_user_email(&self, user_id: Uuid) -> Result<Option<String>, AppError> {
|
||||
sqlx::query_scalar("SELECT email FROM hackathon_users WHERE id = $1")
|
||||
async fn get_user_email(&self, user_id: Uuid) -> Result<Option<String>, AppError> {
|
||||
sqlx::query_scalar("SELECT email FROM hackathon_users WHERE id = $1")
|
||||
.bind(user_id)
|
||||
.fetch_optional(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))
|
||||
}
|
||||
|
||||
async fn team_exists(&self, team_id: Uuid) -> Result<bool, AppError> {
|
||||
sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM hackathon_teams WHERE id = $1)")
|
||||
.bind(team_id)
|
||||
.fetch_one(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))
|
||||
}
|
||||
|
||||
async fn team_has_submission(&self, team_id: Uuid) -> Result<bool, AppError> {
|
||||
sqlx::query_scalar(
|
||||
"SELECT EXISTS(SELECT 1 FROM hackathon_project_submissions WHERE team_id = $1)",
|
||||
)
|
||||
.bind(team_id)
|
||||
.fetch_one(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))
|
||||
}
|
||||
|
||||
async fn user_active_team_name(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
) -> Result<Option<String>, AppError> {
|
||||
sqlx::query_scalar("SELECT t.name FROM hackathon_teams t JOIN hackathon_team_members m ON m.team_id = t.id WHERE m.user_id = $1 AND m.status = 'active' LIMIT 1")
|
||||
.bind(user_id).fetch_optional(self.pool.as_ref()).await.map_err(|e| AppError::InternalServerError(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
async fn team_exists(&self, team_id: Uuid) -> Result<bool, AppError> {
|
||||
sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM hackathon_teams WHERE id = $1)")
|
||||
async fn active_member_count(&self, team_id: Uuid) -> Result<i64, AppError> {
|
||||
sqlx::query_scalar("SELECT COUNT(*) FROM hackathon_team_members WHERE team_id = $1 AND status = 'active'")
|
||||
.bind(team_id).fetch_one(self.pool.as_ref()).await.map_err(|e| AppError::InternalServerError(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
async fn team_has_submission(&self, team_id: Uuid) -> Result<bool, AppError> {
|
||||
sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM hackathon_project_submissions WHERE team_id = $1)")
|
||||
.bind(team_id).fetch_one(self.pool.as_ref()).await.map_err(|e| AppError::InternalServerError(e.to_string()))
|
||||
}
|
||||
|
||||
async fn user_active_team_name(&self, user_id: Uuid) -> Result<Option<String>, AppError> {
|
||||
sqlx::query_scalar("SELECT t.name FROM hackathon_teams t JOIN hackathon_team_members m ON m.team_id = t.id WHERE m.user_id = $1 AND m.status = 'active' LIMIT 1")
|
||||
.bind(user_id).fetch_optional(self.pool.as_ref()).await.map_err(|e| AppError::InternalServerError(e.to_string()))
|
||||
}
|
||||
|
||||
async fn active_member_count(&self, team_id: Uuid) -> Result<i64, AppError> {
|
||||
sqlx::query_scalar("SELECT COUNT(*) FROM hackathon_team_members WHERE team_id = $1 AND status = 'active'")
|
||||
.bind(team_id).fetch_one(self.pool.as_ref()).await.map_err(|e| AppError::InternalServerError(e.to_string()))
|
||||
}
|
||||
|
||||
async fn pending_request_exists(&self, team_id: Uuid, user_id: Uuid) -> Result<bool, AppError> {
|
||||
sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM hackathon_team_join_requests WHERE team_id = $1 AND user_id = $2 AND status = 'pending')")
|
||||
async fn pending_request_exists(
|
||||
&self,
|
||||
team_id: Uuid,
|
||||
user_id: Uuid,
|
||||
) -> Result<bool, AppError> {
|
||||
sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM hackathon_team_join_requests WHERE team_id = $1 AND user_id = $2 AND status = 'pending')")
|
||||
.bind(team_id).bind(user_id).fetch_one(self.pool.as_ref()).await.map_err(|e| AppError::InternalServerError(e.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
pub mod domain;
|
||||
pub mod application;
|
||||
pub mod domain;
|
||||
pub mod infrastructure;
|
||||
|
||||
pub use infrastructure::http::routes::build_join_request_routes;
|
||||
|
||||
Reference in New Issue
Block a user