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,5 +1,5 @@
|
||||
pub mod mentors;
|
||||
pub mod sessions;
|
||||
|
||||
pub use mentors::{mentors_public_routes, mentors_protected_routes};
|
||||
pub use sessions::{sessions_public_routes, sessions_protected_routes};
|
||||
pub mod mentors;
|
||||
pub mod sessions;
|
||||
|
||||
pub use mentors::{mentors_protected_routes, mentors_public_routes};
|
||||
pub use sessions::{sessions_protected_routes, sessions_public_routes};
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
use crate::mentors::domain::{
|
||||
MentorDetail, MentorEntity, MentorListItem, MentorListPage, MentorRepository,
|
||||
};
|
||||
use imphnen_entities::UsersDetailQueryDto;
|
||||
use imphnen_libs::AppState;
|
||||
use imphnen_utils::AppError;
|
||||
use paginator_rs::PaginationParams;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub fn build_detail(
|
||||
entity: &MentorEntity,
|
||||
user: Option<&UsersDetailQueryDto>,
|
||||
) -> MentorDetail {
|
||||
MentorDetail {
|
||||
id: entity.id.to_string(),
|
||||
user_id: entity.user_id.to_string(),
|
||||
fullname: user.map(|u| u.fullname.clone()),
|
||||
email: user.map(|u| u.email.clone()),
|
||||
legal_name: user.and_then(|u| u.legal_name.clone()),
|
||||
gender: user
|
||||
.and_then(|u| u.profile_extension.as_ref())
|
||||
.and_then(|ext| ext.gender.clone()),
|
||||
domicile: user
|
||||
.and_then(|u| u.profile_extension.as_ref())
|
||||
.and_then(|ext| ext.domicile.clone()),
|
||||
phone_for_verification: user
|
||||
.and_then(|u| u.profile_extension.as_ref())
|
||||
.and_then(|ext| ext.phone_for_verification.clone()),
|
||||
bio: user
|
||||
.and_then(|u| u.profile_extension.as_ref())
|
||||
.and_then(|ext| ext.bio.clone()),
|
||||
last_education: user
|
||||
.and_then(|u| u.profile_extension.as_ref())
|
||||
.and_then(|ext| ext.last_education.clone()),
|
||||
linkedin_url: user
|
||||
.and_then(|u| u.profile_extension.as_ref())
|
||||
.and_then(|ext| ext.linkedin_url.clone()),
|
||||
github_url: user
|
||||
.and_then(|u| u.profile_extension.as_ref())
|
||||
.and_then(|ext| ext.github_url.clone()),
|
||||
cv_url: user
|
||||
.and_then(|u| u.profile_extension.as_ref())
|
||||
.and_then(|ext| ext.cv_url.clone()),
|
||||
portfolio_url: user
|
||||
.and_then(|u| u.profile_extension.as_ref())
|
||||
.and_then(|ext| ext.portfolio_url.clone()),
|
||||
industries: entity.industries.clone(),
|
||||
expertise: entity.expertise.clone(),
|
||||
languages: entity.languages.clone(),
|
||||
current_company: entity.current_company.clone(),
|
||||
current_role: entity.current_role.clone(),
|
||||
years_of_experience: entity.years_of_experience,
|
||||
topics_of_interest: entity.topics_of_interest.clone(),
|
||||
preferred_mentee_level: entity.preferred_mentee_level.clone(),
|
||||
preferred_mentoring_formats: entity.preferred_mentoring_formats.clone(),
|
||||
availability_commitment: entity.availability_commitment.clone(),
|
||||
mentoring_rate: entity.mentoring_rate,
|
||||
status: entity.status.clone(),
|
||||
created_at: entity.created_at.to_rfc3339(),
|
||||
updated_at: entity.updated_at.to_rfc3339(),
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MentorQueryService {
|
||||
pub repo: Arc<dyn MentorRepository>,
|
||||
pub state: Arc<AppState>,
|
||||
}
|
||||
|
||||
impl MentorQueryService {
|
||||
pub async fn list(
|
||||
&self,
|
||||
params: PaginationParams,
|
||||
) -> Result<MentorListPage, AppError> {
|
||||
let result = self.repo.find_all(params).await?;
|
||||
|
||||
let mut items: Vec<MentorListItem> = Vec::with_capacity(result.data.len());
|
||||
for entity in &result.data {
|
||||
let mut item = MentorListItem {
|
||||
id: entity.id.to_string(),
|
||||
user_id: entity.user_id.to_string(),
|
||||
fullname: None,
|
||||
email: None,
|
||||
status: entity.status.clone(),
|
||||
created_at: entity.created_at.to_rfc3339(),
|
||||
updated_at: entity.updated_at.to_rfc3339(),
|
||||
};
|
||||
if let Ok(info) = self
|
||||
.state
|
||||
.user_lookup_service
|
||||
.get_user_by_id(entity.user_id, self.state.as_ref())
|
||||
.await
|
||||
{
|
||||
item.fullname = Some(info.basic_info.fullname);
|
||||
item.email = Some(info.basic_info.email);
|
||||
}
|
||||
items.push(item);
|
||||
}
|
||||
|
||||
Ok(paginator_utils::PaginatorResponse {
|
||||
data: items,
|
||||
meta: result.meta,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_by_id(&self, id: Uuid) -> Result<MentorDetail, AppError> {
|
||||
let entity = self.repo.find_by_id(id, false).await?;
|
||||
let user = self
|
||||
.state
|
||||
.user_lookup_service
|
||||
.get_user_by_id(entity.user_id, self.state.as_ref())
|
||||
.await
|
||||
.ok()
|
||||
.map(|i| i.basic_info);
|
||||
Ok(build_detail(&entity, user.as_ref()))
|
||||
}
|
||||
|
||||
pub async fn get_by_email(&self, email: &str) -> Result<MentorDetail, AppError> {
|
||||
let user_dto = self
|
||||
.state
|
||||
.user_lookup_service
|
||||
.get_user_by_email(email, self.state.as_ref())
|
||||
.await
|
||||
.map(|i| i.basic_info)
|
||||
.map_err(|_| AppError::NotFoundError("User not found".to_string()))?;
|
||||
|
||||
let user_id = Uuid::parse_str(&user_dto.id)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
let entity = self.repo.find_by_user_id(user_id, false).await?;
|
||||
Ok(build_detail(&entity, Some(&user_dto)))
|
||||
}
|
||||
|
||||
pub async fn get_status(&self, email: &str) -> Result<String, AppError> {
|
||||
let user_dto = self
|
||||
.state
|
||||
.user_lookup_service
|
||||
.get_user_by_email(email, self.state.as_ref())
|
||||
.await
|
||||
.map(|i| i.basic_info)
|
||||
.map_err(|_| {
|
||||
AppError::NotFoundError(
|
||||
"No mentor application found for current user".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let user_id = Uuid::parse_str(&user_dto.id)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
let entity = self
|
||||
.repo
|
||||
.find_by_user_id(user_id, false)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
AppError::NotFoundError(
|
||||
"No mentor application found for current user".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(entity.status)
|
||||
}
|
||||
|
||||
pub async fn get_entity_by_id(
|
||||
&self,
|
||||
id: Uuid,
|
||||
include_deleted: bool,
|
||||
) -> Result<MentorEntity, AppError> {
|
||||
self.repo.find_by_id(id, include_deleted).await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
use super::mentor_query_service::build_detail;
|
||||
use crate::mentors::domain::{
|
||||
MentorDetail, MentorEntity, MentorRegisterCommand, MentorRegistered,
|
||||
MentorRepository, MentorVerifyCommand,
|
||||
};
|
||||
use imphnen_entities::{RolesDetailQueryDto, users::UserProfileExtensionDto};
|
||||
use imphnen_iam::roles::domain::RoleRepository;
|
||||
use imphnen_iam::users::domain::{UserEntity, UserRepository};
|
||||
use imphnen_libs::{AppState, hash_password};
|
||||
use imphnen_utils::AppError;
|
||||
use std::sync::Arc;
|
||||
use tracing::error;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct MentorRegistrationService {
|
||||
pub repo: Arc<dyn MentorRepository>,
|
||||
pub state: Arc<AppState>,
|
||||
pub user_repo: Arc<dyn UserRepository>,
|
||||
pub role_repo: Arc<dyn RoleRepository>,
|
||||
}
|
||||
|
||||
impl MentorRegistrationService {
|
||||
pub async fn register(
|
||||
&self,
|
||||
cmd: MentorRegisterCommand,
|
||||
) -> Result<MentorRegistered, AppError> {
|
||||
let user_email = cmd.email.clone();
|
||||
|
||||
let user_id: Uuid = match self.user_repo.find_by_email(user_email.clone()).await
|
||||
{
|
||||
Ok(mut entity) => {
|
||||
let existing_user_id = Uuid::parse_str(&entity.id)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
if self
|
||||
.repo
|
||||
.find_by_user_id(existing_user_id, false)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
return Err(AppError::ConflictError(
|
||||
"Mentor profile already exists for this user".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mentor_role = self
|
||||
.role_repo
|
||||
.find_by_name("Mentor".to_string())
|
||||
.await
|
||||
.map_err(|_| {
|
||||
AppError::BadRequestError("Mentor Role Not Found".to_string())
|
||||
})?;
|
||||
|
||||
entity.fullname = cmd.fullname.clone();
|
||||
entity.is_active = false;
|
||||
entity.role = RolesDetailQueryDto {
|
||||
id: mentor_role.id.to_string(),
|
||||
name: mentor_role.name.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
entity.password = hash_password(&cmd.password).map_err(|e| {
|
||||
error!("Failed to hash password for {}: {}", user_email, e);
|
||||
AppError::InternalServerError("Failed to hash password".to_string())
|
||||
})?;
|
||||
|
||||
let mut profile_ext = entity.profile_extension.clone().unwrap_or_default();
|
||||
profile_ext.phone_number = cmd.phone_number.clone();
|
||||
profile_ext.phone_for_verification = cmd.phone_for_verification.clone();
|
||||
profile_ext.gender = cmd.gender.clone();
|
||||
profile_ext.domicile = cmd.domicile.clone();
|
||||
profile_ext.bio = Some(cmd.bio.clone());
|
||||
profile_ext.last_education = cmd.last_education.clone();
|
||||
profile_ext.linkedin_url = cmd.linkedin_url.clone();
|
||||
profile_ext.github_url = cmd.github_url.clone();
|
||||
profile_ext.cv_url = cmd.cv_url.clone();
|
||||
profile_ext.portfolio_url = cmd.portfolio_url.clone();
|
||||
entity.profile_extension = Some(profile_ext);
|
||||
|
||||
let uid_str = entity.id.clone();
|
||||
self.user_repo.update(entity).await.map_err(|e| {
|
||||
error!("Failed to update user {} to mentor role: {}", user_email, e);
|
||||
AppError::InternalServerError(e.to_string())
|
||||
})?;
|
||||
|
||||
Uuid::parse_str(&uid_str)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||
}
|
||||
Err(_) => {
|
||||
let mentor_role = self
|
||||
.role_repo
|
||||
.find_by_name("Mentor".to_string())
|
||||
.await
|
||||
.map_err(|_| {
|
||||
AppError::BadRequestError("Mentor Role Not Found".to_string())
|
||||
})?;
|
||||
|
||||
let hashed_password = hash_password(&cmd.password).map_err(|e| {
|
||||
error!("Failed to hash password for new user {}: {}", user_email, e);
|
||||
AppError::InternalServerError("Failed to hash password".to_string())
|
||||
})?;
|
||||
|
||||
let new_user_id = Uuid::new_v4();
|
||||
let profile_ext = UserProfileExtensionDto {
|
||||
phone_number: cmd.phone_number.clone(),
|
||||
phone_for_verification: cmd.phone_for_verification.clone(),
|
||||
gender: cmd.gender.clone(),
|
||||
domicile: cmd.domicile.clone(),
|
||||
bio: Some(cmd.bio.clone()),
|
||||
last_education: cmd.last_education.clone(),
|
||||
linkedin_url: cmd.linkedin_url.clone(),
|
||||
github_url: cmd.github_url.clone(),
|
||||
cv_url: cmd.cv_url.clone(),
|
||||
portfolio_url: cmd.portfolio_url.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
let new_entity = UserEntity {
|
||||
id: new_user_id.to_string(),
|
||||
email: cmd.email.clone(),
|
||||
fullname: cmd.fullname.clone(),
|
||||
legal_name: Some(cmd.legal_name.clone()),
|
||||
password: hashed_password,
|
||||
is_active: false,
|
||||
role: RolesDetailQueryDto {
|
||||
id: mentor_role.id.to_string(),
|
||||
name: mentor_role.name.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
profile_extension: Some(profile_ext),
|
||||
created_at: imphnen_utils::get_iso_date(),
|
||||
updated_at: imphnen_utils::get_iso_date(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
self.user_repo.create(new_entity).await.map_err(|e| {
|
||||
error!("Failed to create new user {}: {}", user_email, e);
|
||||
AppError::InternalServerError(e.to_string())
|
||||
})?;
|
||||
|
||||
new_user_id
|
||||
}
|
||||
};
|
||||
|
||||
let new_entity = MentorEntity {
|
||||
id: Uuid::new_v4(),
|
||||
user_id,
|
||||
industries: cmd.industries.clone(),
|
||||
expertise: cmd.expertise.clone(),
|
||||
languages: cmd.languages.clone(),
|
||||
current_company: cmd.current_company.clone(),
|
||||
current_role: cmd.current_role.clone(),
|
||||
years_of_experience: cmd.years_of_experience,
|
||||
topics_of_interest: cmd.topics_of_interest.clone(),
|
||||
preferred_mentee_level: cmd.preferred_mentee_level.clone(),
|
||||
preferred_mentoring_formats: cmd.preferred_mentoring_formats.clone(),
|
||||
availability_commitment: cmd.availability_commitment.clone(),
|
||||
mentoring_rate: cmd.mentoring_rate_amount as f64,
|
||||
status: "pending".to_string(),
|
||||
is_deleted: false,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let mentor_id = self.repo.create(new_entity.clone()).await.map_err(|e| {
|
||||
error!("Failed to create mentor profile for {}: {}", user_email, e);
|
||||
e
|
||||
})?;
|
||||
|
||||
Ok(MentorRegistered {
|
||||
id: mentor_id.to_string(),
|
||||
user_id: user_id.to_string(),
|
||||
email: Some(user_email),
|
||||
status: "pending".to_string(),
|
||||
created_at: new_entity.created_at.to_rfc3339(),
|
||||
updated_at: new_entity.updated_at.to_rfc3339(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn verify(
|
||||
&self,
|
||||
id: Uuid,
|
||||
cmd: MentorVerifyCommand,
|
||||
) -> Result<MentorDetail, AppError> {
|
||||
let mut entity = self.repo.find_by_id(id, false).await?;
|
||||
entity.status = cmd.status;
|
||||
entity.updated_at = chrono::Utc::now();
|
||||
|
||||
self.repo.update(entity).await?;
|
||||
|
||||
let updated = self.repo.find_by_id(id, false).await?;
|
||||
let user = self
|
||||
.state
|
||||
.user_lookup_service
|
||||
.get_user_by_id(updated.user_id, self.state.as_ref())
|
||||
.await
|
||||
.ok()
|
||||
.map(|i| i.basic_info);
|
||||
Ok(build_detail(&updated, user.as_ref()))
|
||||
}
|
||||
|
||||
pub async fn delete(&self, id: Uuid) -> Result<(), AppError> {
|
||||
self.repo.soft_delete(id).await
|
||||
}
|
||||
}
|
||||
@@ -1,411 +1,113 @@
|
||||
use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use paginator_rs::PaginationParams;
|
||||
use paginator_utils::PaginatorResponse;
|
||||
use uuid::Uuid;
|
||||
use imphnen_utils::AppError;
|
||||
use imphnen_libs::{AppState, hash_password};
|
||||
use imphnen_entities::{RolesDetailQueryDto, users::UserProfileExtensionDto};
|
||||
use imphnen_iam::users::domain::{UserRepository, UserEntity};
|
||||
use imphnen_iam::roles::domain::RoleRepository;
|
||||
use tracing::error;
|
||||
use crate::mentors::domain::{MentorEntity, MentorRepository, MentorService};
|
||||
use crate::mentors::infrastructure::http::dto::{
|
||||
MentorDetailResponseDto, MentorListResponseDto, MentorRegisterResponseDto,
|
||||
MentorUpdateRequestDto, MentorUserRegisterRequestDto, MentorVerifyRequestDto,
|
||||
use super::mentor_query_service::MentorQueryService;
|
||||
use super::mentor_registration_service::MentorRegistrationService;
|
||||
use super::mentor_update_service::MentorUpdateService;
|
||||
use crate::mentors::domain::{
|
||||
MentorDetail, MentorEntity, MentorListPage, MentorRegisterCommand,
|
||||
MentorRegistered, MentorRepository, MentorService, MentorUpdateCommand,
|
||||
MentorVerifyCommand,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use imphnen_iam::roles::domain::RoleRepository;
|
||||
use imphnen_iam::users::domain::UserRepository;
|
||||
use imphnen_libs::AppState;
|
||||
use imphnen_utils::AppError;
|
||||
use paginator_rs::PaginationParams;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct MentorServiceImpl {
|
||||
repo: Arc<dyn MentorRepository>,
|
||||
state: Arc<AppState>,
|
||||
user_repo: Arc<dyn UserRepository>,
|
||||
role_repo: Arc<dyn RoleRepository>,
|
||||
query: MentorQueryService,
|
||||
registration: MentorRegistrationService,
|
||||
update: MentorUpdateService,
|
||||
}
|
||||
|
||||
impl MentorServiceImpl {
|
||||
pub fn new(
|
||||
repo: Arc<dyn MentorRepository>,
|
||||
state: Arc<AppState>,
|
||||
user_repo: Arc<dyn UserRepository>,
|
||||
role_repo: Arc<dyn RoleRepository>,
|
||||
) -> Self {
|
||||
Self { repo, state, user_repo, role_repo }
|
||||
}
|
||||
|
||||
fn build_detail_response(
|
||||
entity: &MentorEntity,
|
||||
user: Option<&imphnen_entities::UsersDetailQueryDto>,
|
||||
) -> MentorDetailResponseDto {
|
||||
MentorDetailResponseDto {
|
||||
id: entity.id.to_string(),
|
||||
user_id: entity.user_id.to_string(),
|
||||
fullname: user.map(|u| u.fullname.clone()),
|
||||
email: user.map(|u| u.email.clone()),
|
||||
legal_name: user.and_then(|u| u.legal_name.clone()),
|
||||
gender: user
|
||||
.and_then(|u| u.profile_extension.as_ref())
|
||||
.and_then(|ext| ext.gender.clone()),
|
||||
domicile: user
|
||||
.and_then(|u| u.profile_extension.as_ref())
|
||||
.and_then(|ext| ext.domicile.clone()),
|
||||
phone_for_verification: user
|
||||
.and_then(|u| u.profile_extension.as_ref())
|
||||
.and_then(|ext| ext.phone_for_verification.clone()),
|
||||
bio: user
|
||||
.and_then(|u| u.profile_extension.as_ref())
|
||||
.and_then(|ext| ext.bio.clone()),
|
||||
last_education: user
|
||||
.and_then(|u| u.profile_extension.as_ref())
|
||||
.and_then(|ext| ext.last_education.clone()),
|
||||
linkedin_url: user
|
||||
.and_then(|u| u.profile_extension.as_ref())
|
||||
.and_then(|ext| ext.linkedin_url.clone()),
|
||||
github_url: user
|
||||
.and_then(|u| u.profile_extension.as_ref())
|
||||
.and_then(|ext| ext.github_url.clone()),
|
||||
cv_url: user
|
||||
.and_then(|u| u.profile_extension.as_ref())
|
||||
.and_then(|ext| ext.cv_url.clone()),
|
||||
portfolio_url: user
|
||||
.and_then(|u| u.profile_extension.as_ref())
|
||||
.and_then(|ext| ext.portfolio_url.clone()),
|
||||
industries: entity.industries.clone(),
|
||||
expertise: entity.expertise.clone(),
|
||||
languages: entity.languages.clone(),
|
||||
current_company: entity.current_company.clone(),
|
||||
current_role: entity.current_role.clone(),
|
||||
years_of_experience: entity.years_of_experience,
|
||||
topics_of_interest: entity.topics_of_interest.clone(),
|
||||
preferred_mentee_level: entity.preferred_mentee_level.clone(),
|
||||
preferred_mentoring_formats: entity.preferred_mentoring_formats.clone(),
|
||||
availability_commitment: entity.availability_commitment.clone(),
|
||||
mentoring_rate: entity.mentoring_rate,
|
||||
status: entity.status.clone(),
|
||||
created_at: entity.created_at.to_rfc3339(),
|
||||
updated_at: entity.updated_at.to_rfc3339(),
|
||||
}
|
||||
}
|
||||
pub fn new(
|
||||
repo: Arc<dyn MentorRepository>,
|
||||
state: Arc<AppState>,
|
||||
user_repo: Arc<dyn UserRepository>,
|
||||
role_repo: Arc<dyn RoleRepository>,
|
||||
) -> Self {
|
||||
Self {
|
||||
query: MentorQueryService {
|
||||
repo: Arc::clone(&repo),
|
||||
state: Arc::clone(&state),
|
||||
},
|
||||
registration: MentorRegistrationService {
|
||||
repo: Arc::clone(&repo),
|
||||
state: Arc::clone(&state),
|
||||
user_repo,
|
||||
role_repo,
|
||||
},
|
||||
update: MentorUpdateService {
|
||||
repo: Arc::clone(&repo),
|
||||
state: Arc::clone(&state),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl MentorService for MentorServiceImpl {
|
||||
async fn list(
|
||||
&self,
|
||||
params: PaginationParams,
|
||||
) -> Result<PaginatorResponse<MentorListResponseDto>, AppError> {
|
||||
let result = self.repo.find_all(params).await?;
|
||||
async fn list(
|
||||
&self,
|
||||
params: PaginationParams,
|
||||
) -> Result<MentorListPage, AppError> {
|
||||
self.query.list(params).await
|
||||
}
|
||||
|
||||
let mut items: Vec<MentorListResponseDto> = Vec::with_capacity(result.data.len());
|
||||
for entity in &result.data {
|
||||
let mut item = MentorListResponseDto {
|
||||
id: entity.id.to_string(),
|
||||
user_id: entity.user_id.to_string(),
|
||||
fullname: None,
|
||||
email: None,
|
||||
status: entity.status.clone(),
|
||||
created_at: entity.created_at.to_rfc3339(),
|
||||
updated_at: entity.updated_at.to_rfc3339(),
|
||||
};
|
||||
if let Ok(info) = self.state.user_lookup_service
|
||||
.get_user_by_id(entity.user_id, self.state.as_ref())
|
||||
.await
|
||||
{
|
||||
item.fullname = Some(info.basic_info.fullname);
|
||||
item.email = Some(info.basic_info.email);
|
||||
}
|
||||
items.push(item);
|
||||
}
|
||||
async fn get_by_id(&self, id: Uuid) -> Result<MentorDetail, AppError> {
|
||||
self.query.get_by_id(id).await
|
||||
}
|
||||
|
||||
Ok(PaginatorResponse { data: items, meta: result.meta })
|
||||
}
|
||||
async fn get_by_email(&self, email: &str) -> Result<MentorDetail, AppError> {
|
||||
self.query.get_by_email(email).await
|
||||
}
|
||||
|
||||
async fn get_by_id(&self, id: Uuid) -> Result<MentorDetailResponseDto, AppError> {
|
||||
let entity = self.repo.find_by_id(id, false).await?;
|
||||
let user = self.state.user_lookup_service
|
||||
.get_user_by_id(entity.user_id, self.state.as_ref())
|
||||
.await
|
||||
.ok()
|
||||
.map(|i| i.basic_info);
|
||||
Ok(Self::build_detail_response(&entity, user.as_ref()))
|
||||
}
|
||||
async fn register(
|
||||
&self,
|
||||
cmd: MentorRegisterCommand,
|
||||
) -> Result<MentorRegistered, AppError> {
|
||||
self.registration.register(cmd).await
|
||||
}
|
||||
|
||||
async fn get_by_email(&self, email: &str) -> Result<MentorDetailResponseDto, AppError> {
|
||||
let user_dto = self.state.user_lookup_service
|
||||
.get_user_by_email(email, self.state.as_ref())
|
||||
.await
|
||||
.map(|i| i.basic_info)
|
||||
.map_err(|_| AppError::NotFoundError("User not found".to_string()))?;
|
||||
async fn update(
|
||||
&self,
|
||||
id: Uuid,
|
||||
cmd: MentorUpdateCommand,
|
||||
) -> Result<MentorDetail, AppError> {
|
||||
self.update.update(id, cmd).await
|
||||
}
|
||||
|
||||
let user_id = Uuid::parse_str(&user_dto.id)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
async fn update_me(
|
||||
&self,
|
||||
email: &str,
|
||||
cmd: MentorUpdateCommand,
|
||||
) -> Result<MentorDetail, AppError> {
|
||||
self.update.update_me(email, cmd).await
|
||||
}
|
||||
|
||||
let entity = self.repo.find_by_user_id(user_id, false).await?;
|
||||
Ok(Self::build_detail_response(&entity, Some(&user_dto)))
|
||||
}
|
||||
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
|
||||
self.registration.delete(id).await
|
||||
}
|
||||
|
||||
async fn register(
|
||||
&self,
|
||||
dto: MentorUserRegisterRequestDto,
|
||||
) -> Result<MentorRegisterResponseDto, AppError> {
|
||||
let user_email = dto.email.clone();
|
||||
async fn verify(
|
||||
&self,
|
||||
id: Uuid,
|
||||
cmd: MentorVerifyCommand,
|
||||
) -> Result<MentorDetail, AppError> {
|
||||
self.registration.verify(id, cmd).await
|
||||
}
|
||||
|
||||
let user_id: Uuid = match self.user_repo.find_by_email(user_email.clone()).await {
|
||||
Ok(mut entity) => {
|
||||
let existing_user_id = Uuid::parse_str(&entity.id)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
async fn get_status(&self, email: &str) -> Result<String, AppError> {
|
||||
self.query.get_status(email).await
|
||||
}
|
||||
|
||||
if self.repo.find_by_user_id(existing_user_id, false).await.is_ok() {
|
||||
return Err(AppError::ConflictError(
|
||||
"Mentor profile already exists for this user".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mentor_role = self.role_repo
|
||||
.find_by_name("Mentor".to_string())
|
||||
.await
|
||||
.map_err(|_| AppError::BadRequestError("Mentor Role Not Found".to_string()))?;
|
||||
|
||||
entity.fullname = dto.fullname.clone();
|
||||
entity.is_active = false;
|
||||
entity.role = RolesDetailQueryDto {
|
||||
id: mentor_role.id.to_string(),
|
||||
name: mentor_role.name.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
entity.password = hash_password(&dto.password).map_err(|e| {
|
||||
error!("Failed to hash password for {}: {}", user_email, e);
|
||||
AppError::InternalServerError("Failed to hash password".to_string())
|
||||
})?;
|
||||
|
||||
let mut profile_ext = entity.profile_extension.clone().unwrap_or_default();
|
||||
profile_ext.phone_number = dto.phone_number.clone();
|
||||
profile_ext.phone_for_verification = dto.identity_and_verification.phone_for_verification.clone();
|
||||
profile_ext.gender = dto.identity_and_verification.gender.clone();
|
||||
profile_ext.domicile = dto.identity_and_verification.domicile.clone();
|
||||
profile_ext.bio = Some(dto.professional_profile.bio.clone());
|
||||
profile_ext.last_education = dto.professional_profile.last_education.clone();
|
||||
profile_ext.linkedin_url = dto.professional_profile.linkedin_url.clone();
|
||||
profile_ext.github_url = dto.professional_profile.github_url.clone();
|
||||
profile_ext.cv_url = dto.professional_profile.cv_url.clone();
|
||||
profile_ext.portfolio_url = dto.professional_profile.portfolio_url.clone();
|
||||
entity.profile_extension = Some(profile_ext);
|
||||
|
||||
let uid_str = entity.id.clone();
|
||||
self.user_repo.update(entity).await.map_err(|e| {
|
||||
error!("Failed to update user {} to mentor role: {}", user_email, e);
|
||||
AppError::InternalServerError(e.to_string())
|
||||
})?;
|
||||
|
||||
Uuid::parse_str(&uid_str)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||
}
|
||||
Err(_) => {
|
||||
let mentor_role = self.role_repo
|
||||
.find_by_name("Mentor".to_string())
|
||||
.await
|
||||
.map_err(|_| AppError::BadRequestError("Mentor Role Not Found".to_string()))?;
|
||||
|
||||
let hashed_password = hash_password(&dto.password).map_err(|e| {
|
||||
error!("Failed to hash password for new user {}: {}", user_email, e);
|
||||
AppError::InternalServerError("Failed to hash password".to_string())
|
||||
})?;
|
||||
|
||||
let new_user_id = Uuid::new_v4();
|
||||
let profile_ext = UserProfileExtensionDto {
|
||||
phone_number: dto.phone_number.clone(),
|
||||
phone_for_verification: dto.identity_and_verification.phone_for_verification.clone(),
|
||||
gender: dto.identity_and_verification.gender.clone(),
|
||||
domicile: dto.identity_and_verification.domicile.clone(),
|
||||
bio: Some(dto.professional_profile.bio.clone()),
|
||||
last_education: dto.professional_profile.last_education.clone(),
|
||||
linkedin_url: dto.professional_profile.linkedin_url.clone(),
|
||||
github_url: dto.professional_profile.github_url.clone(),
|
||||
cv_url: dto.professional_profile.cv_url.clone(),
|
||||
portfolio_url: dto.professional_profile.portfolio_url.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
let new_entity = UserEntity {
|
||||
id: new_user_id.to_string(),
|
||||
email: dto.email.clone(),
|
||||
fullname: dto.fullname.clone(),
|
||||
legal_name: Some(dto.identity_and_verification.legal_name.clone()),
|
||||
password: hashed_password,
|
||||
is_active: false,
|
||||
role: RolesDetailQueryDto {
|
||||
id: mentor_role.id.to_string(),
|
||||
name: mentor_role.name.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
profile_extension: Some(profile_ext),
|
||||
created_at: imphnen_utils::get_iso_date(),
|
||||
updated_at: imphnen_utils::get_iso_date(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
self.user_repo.create(new_entity).await.map_err(|e| {
|
||||
error!("Failed to create new user {}: {}", user_email, e);
|
||||
AppError::InternalServerError(e.to_string())
|
||||
})?;
|
||||
|
||||
new_user_id
|
||||
}
|
||||
};
|
||||
|
||||
let new_entity = MentorEntity {
|
||||
id: Uuid::new_v4(),
|
||||
user_id,
|
||||
industries: dto.professional_profile.industries.clone(),
|
||||
expertise: dto.professional_profile.expertise.clone(),
|
||||
languages: dto.professional_profile.languages.clone(),
|
||||
current_company: dto.professional_profile.current_company.clone(),
|
||||
current_role: dto.professional_profile.current_role.clone(),
|
||||
years_of_experience: dto.professional_profile.years_of_experience,
|
||||
topics_of_interest: dto.mentoring_logistics.topics_of_interest.clone(),
|
||||
preferred_mentee_level: dto.mentoring_logistics.preferred_mentee_level.clone(),
|
||||
preferred_mentoring_formats: dto.mentoring_logistics.preferred_mentoring_formats.clone(),
|
||||
availability_commitment: dto.mentoring_logistics.availability_commitment.clone(),
|
||||
mentoring_rate: dto.mentoring_logistics.mentoring_rate_amount as f64,
|
||||
status: "pending".to_string(),
|
||||
is_deleted: false,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let mentor_id = self.repo.create(new_entity.clone()).await.map_err(|e| {
|
||||
error!("Failed to create mentor profile for {}: {}", user_email, e);
|
||||
e
|
||||
})?;
|
||||
|
||||
Ok(MentorRegisterResponseDto {
|
||||
id: mentor_id.to_string(),
|
||||
user_id: user_id.to_string(),
|
||||
email: Some(user_email),
|
||||
status: "pending".to_string(),
|
||||
created_at: new_entity.created_at.to_rfc3339(),
|
||||
updated_at: new_entity.updated_at.to_rfc3339(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn update(
|
||||
&self,
|
||||
id: Uuid,
|
||||
dto: MentorUpdateRequestDto,
|
||||
) -> Result<MentorDetailResponseDto, AppError> {
|
||||
let mut entity = self.repo.find_by_id(id, false).await?;
|
||||
|
||||
if let Some(val) = dto.industries { entity.industries = val; }
|
||||
if let Some(val) = dto.expertise { entity.expertise = val; }
|
||||
if let Some(val) = dto.languages { entity.languages = val; }
|
||||
if let Some(val) = dto.current_company { entity.current_company = val; }
|
||||
if let Some(val) = dto.current_role { entity.current_role = val; }
|
||||
if let Some(val) = dto.years_of_experience { entity.years_of_experience = val; }
|
||||
if let Some(val) = dto.topics_of_interest { entity.topics_of_interest = val; }
|
||||
if let Some(val) = dto.preferred_mentee_level { entity.preferred_mentee_level = val; }
|
||||
if let Some(val) = dto.preferred_mentoring_formats { entity.preferred_mentoring_formats = val; }
|
||||
if let Some(val) = dto.availability_commitment { entity.availability_commitment = val; }
|
||||
if let Some(val) = dto.mentoring_rate_amount { entity.mentoring_rate = val as f64; }
|
||||
entity.updated_at = chrono::Utc::now();
|
||||
|
||||
self.repo.update(entity).await?;
|
||||
|
||||
let updated = self.repo.find_by_id(id, false).await?;
|
||||
let user = self.state.user_lookup_service
|
||||
.get_user_by_id(updated.user_id, self.state.as_ref())
|
||||
.await
|
||||
.ok()
|
||||
.map(|i| i.basic_info);
|
||||
Ok(Self::build_detail_response(&updated, user.as_ref()))
|
||||
}
|
||||
|
||||
async fn update_me(
|
||||
&self,
|
||||
email: &str,
|
||||
dto: MentorUpdateRequestDto,
|
||||
) -> Result<MentorDetailResponseDto, AppError> {
|
||||
let user_dto = self.state.user_lookup_service
|
||||
.get_user_by_email(email, self.state.as_ref())
|
||||
.await
|
||||
.map(|i| i.basic_info)
|
||||
.map_err(|_| AppError::NotFoundError("User not found".to_string()))?;
|
||||
|
||||
let user_id = Uuid::parse_str(&user_dto.id)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
let mut entity = self.repo.find_by_user_id(user_id, false).await?;
|
||||
|
||||
if let Some(val) = dto.industries { entity.industries = val; }
|
||||
if let Some(val) = dto.expertise { entity.expertise = val; }
|
||||
if let Some(val) = dto.languages { entity.languages = val; }
|
||||
if let Some(val) = dto.current_company { entity.current_company = val; }
|
||||
if let Some(val) = dto.current_role { entity.current_role = val; }
|
||||
if let Some(val) = dto.years_of_experience { entity.years_of_experience = val; }
|
||||
if let Some(val) = dto.topics_of_interest { entity.topics_of_interest = val; }
|
||||
if let Some(val) = dto.preferred_mentee_level { entity.preferred_mentee_level = val; }
|
||||
if let Some(val) = dto.preferred_mentoring_formats { entity.preferred_mentoring_formats = val; }
|
||||
if let Some(val) = dto.availability_commitment { entity.availability_commitment = val; }
|
||||
if let Some(val) = dto.mentoring_rate_amount { entity.mentoring_rate = val as f64; }
|
||||
entity.updated_at = chrono::Utc::now();
|
||||
|
||||
let entity_id = entity.id;
|
||||
self.repo.update(entity).await?;
|
||||
|
||||
let updated = self.repo.find_by_id(entity_id, false).await?;
|
||||
let refreshed_user = self.state.user_lookup_service
|
||||
.get_user_by_id(updated.user_id, self.state.as_ref())
|
||||
.await
|
||||
.ok()
|
||||
.map(|i| i.basic_info);
|
||||
Ok(Self::build_detail_response(&updated, refreshed_user.as_ref()))
|
||||
}
|
||||
|
||||
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
|
||||
self.repo.soft_delete(id).await
|
||||
}
|
||||
|
||||
async fn verify(
|
||||
&self,
|
||||
id: Uuid,
|
||||
dto: MentorVerifyRequestDto,
|
||||
) -> Result<MentorDetailResponseDto, AppError> {
|
||||
let mut entity = self.repo.find_by_id(id, false).await?;
|
||||
entity.status = dto.status;
|
||||
entity.updated_at = chrono::Utc::now();
|
||||
|
||||
self.repo.update(entity).await?;
|
||||
|
||||
let updated = self.repo.find_by_id(id, false).await?;
|
||||
let user = self.state.user_lookup_service
|
||||
.get_user_by_id(updated.user_id, self.state.as_ref())
|
||||
.await
|
||||
.ok()
|
||||
.map(|i| i.basic_info);
|
||||
Ok(Self::build_detail_response(&updated, user.as_ref()))
|
||||
}
|
||||
|
||||
async fn get_status(&self, email: &str) -> Result<String, AppError> {
|
||||
let user_dto = self.state.user_lookup_service
|
||||
.get_user_by_email(email, self.state.as_ref())
|
||||
.await
|
||||
.map(|i| i.basic_info)
|
||||
.map_err(|_| {
|
||||
AppError::NotFoundError("No mentor application found for current user".to_string())
|
||||
})?;
|
||||
|
||||
let user_id = Uuid::parse_str(&user_dto.id)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
let entity = self.repo.find_by_user_id(user_id, false).await.map_err(|_| {
|
||||
AppError::NotFoundError("No mentor application found for current user".to_string())
|
||||
})?;
|
||||
|
||||
Ok(entity.status)
|
||||
}
|
||||
|
||||
async fn get_entity_by_id(
|
||||
&self,
|
||||
id: Uuid,
|
||||
include_deleted: bool,
|
||||
) -> Result<MentorEntity, AppError> {
|
||||
self.repo.find_by_id(id, include_deleted).await
|
||||
}
|
||||
async fn get_entity_by_id(
|
||||
&self,
|
||||
id: Uuid,
|
||||
include_deleted: bool,
|
||||
) -> Result<MentorEntity, AppError> {
|
||||
self.query.get_entity_by_id(id, include_deleted).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
use super::mentor_query_service::build_detail;
|
||||
use crate::mentors::domain::{MentorDetail, MentorRepository, MentorUpdateCommand};
|
||||
use imphnen_libs::AppState;
|
||||
use imphnen_utils::AppError;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct MentorUpdateService {
|
||||
pub repo: Arc<dyn MentorRepository>,
|
||||
pub state: Arc<AppState>,
|
||||
}
|
||||
|
||||
impl MentorUpdateService {
|
||||
pub async fn update(
|
||||
&self,
|
||||
id: Uuid,
|
||||
cmd: MentorUpdateCommand,
|
||||
) -> Result<MentorDetail, AppError> {
|
||||
let mut entity = self.repo.find_by_id(id, false).await?;
|
||||
|
||||
if let Some(val) = cmd.industries {
|
||||
entity.industries = val;
|
||||
}
|
||||
if let Some(val) = cmd.expertise {
|
||||
entity.expertise = val;
|
||||
}
|
||||
if let Some(val) = cmd.languages {
|
||||
entity.languages = val;
|
||||
}
|
||||
if let Some(val) = cmd.current_company {
|
||||
entity.current_company = val;
|
||||
}
|
||||
if let Some(val) = cmd.current_role {
|
||||
entity.current_role = val;
|
||||
}
|
||||
if let Some(val) = cmd.years_of_experience {
|
||||
entity.years_of_experience = val;
|
||||
}
|
||||
if let Some(val) = cmd.topics_of_interest {
|
||||
entity.topics_of_interest = val;
|
||||
}
|
||||
if let Some(val) = cmd.preferred_mentee_level {
|
||||
entity.preferred_mentee_level = val;
|
||||
}
|
||||
if let Some(val) = cmd.preferred_mentoring_formats {
|
||||
entity.preferred_mentoring_formats = val;
|
||||
}
|
||||
if let Some(val) = cmd.availability_commitment {
|
||||
entity.availability_commitment = val;
|
||||
}
|
||||
if let Some(val) = cmd.mentoring_rate_amount {
|
||||
entity.mentoring_rate = val as f64;
|
||||
}
|
||||
entity.updated_at = chrono::Utc::now();
|
||||
|
||||
self.repo.update(entity).await?;
|
||||
|
||||
let updated = self.repo.find_by_id(id, false).await?;
|
||||
let user = self
|
||||
.state
|
||||
.user_lookup_service
|
||||
.get_user_by_id(updated.user_id, self.state.as_ref())
|
||||
.await
|
||||
.ok()
|
||||
.map(|i| i.basic_info);
|
||||
Ok(build_detail(&updated, user.as_ref()))
|
||||
}
|
||||
|
||||
pub async fn update_me(
|
||||
&self,
|
||||
email: &str,
|
||||
cmd: MentorUpdateCommand,
|
||||
) -> Result<MentorDetail, AppError> {
|
||||
let user_dto = self
|
||||
.state
|
||||
.user_lookup_service
|
||||
.get_user_by_email(email, self.state.as_ref())
|
||||
.await
|
||||
.map(|i| i.basic_info)
|
||||
.map_err(|_| AppError::NotFoundError("User not found".to_string()))?;
|
||||
|
||||
let user_id = Uuid::parse_str(&user_dto.id)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
let mut entity = self.repo.find_by_user_id(user_id, false).await?;
|
||||
|
||||
if let Some(val) = cmd.industries {
|
||||
entity.industries = val;
|
||||
}
|
||||
if let Some(val) = cmd.expertise {
|
||||
entity.expertise = val;
|
||||
}
|
||||
if let Some(val) = cmd.languages {
|
||||
entity.languages = val;
|
||||
}
|
||||
if let Some(val) = cmd.current_company {
|
||||
entity.current_company = val;
|
||||
}
|
||||
if let Some(val) = cmd.current_role {
|
||||
entity.current_role = val;
|
||||
}
|
||||
if let Some(val) = cmd.years_of_experience {
|
||||
entity.years_of_experience = val;
|
||||
}
|
||||
if let Some(val) = cmd.topics_of_interest {
|
||||
entity.topics_of_interest = val;
|
||||
}
|
||||
if let Some(val) = cmd.preferred_mentee_level {
|
||||
entity.preferred_mentee_level = val;
|
||||
}
|
||||
if let Some(val) = cmd.preferred_mentoring_formats {
|
||||
entity.preferred_mentoring_formats = val;
|
||||
}
|
||||
if let Some(val) = cmd.availability_commitment {
|
||||
entity.availability_commitment = val;
|
||||
}
|
||||
if let Some(val) = cmd.mentoring_rate_amount {
|
||||
entity.mentoring_rate = val as f64;
|
||||
}
|
||||
entity.updated_at = chrono::Utc::now();
|
||||
|
||||
let entity_id = entity.id;
|
||||
self.repo.update(entity).await?;
|
||||
|
||||
let updated = self.repo.find_by_id(entity_id, false).await?;
|
||||
let refreshed_user = self
|
||||
.state
|
||||
.user_lookup_service
|
||||
.get_user_by_id(updated.user_id, self.state.as_ref())
|
||||
.await
|
||||
.ok()
|
||||
.map(|i| i.basic_info);
|
||||
Ok(build_detail(&updated, refreshed_user.as_ref()))
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
pub mod mentor_query_service;
|
||||
pub mod mentor_registration_service;
|
||||
pub mod mentor_service;
|
||||
pub mod mentor_update_service;
|
||||
|
||||
pub use mentor_service::MentorServiceImpl;
|
||||
|
||||
@@ -2,21 +2,21 @@ use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MentorEntity {
|
||||
pub id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub industries: Vec<String>,
|
||||
pub expertise: Vec<String>,
|
||||
pub languages: Vec<String>,
|
||||
pub current_company: String,
|
||||
pub current_role: String,
|
||||
pub years_of_experience: i32,
|
||||
pub topics_of_interest: Vec<String>,
|
||||
pub preferred_mentee_level: Vec<String>,
|
||||
pub preferred_mentoring_formats: Vec<String>,
|
||||
pub availability_commitment: String,
|
||||
pub mentoring_rate: f64,
|
||||
pub status: String,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
pub id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub industries: Vec<String>,
|
||||
pub expertise: Vec<String>,
|
||||
pub languages: Vec<String>,
|
||||
pub current_company: String,
|
||||
pub current_role: String,
|
||||
pub years_of_experience: i32,
|
||||
pub topics_of_interest: Vec<String>,
|
||||
pub preferred_mentee_level: Vec<String>,
|
||||
pub preferred_mentoring_formats: Vec<String>,
|
||||
pub availability_commitment: String,
|
||||
pub mentoring_rate: f64,
|
||||
pub status: String,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
use paginator_utils::PaginatorResponse;
|
||||
|
||||
pub struct MentorListItem {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
pub fullname: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
pub struct MentorDetail {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
pub fullname: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub legal_name: Option<String>,
|
||||
pub gender: Option<String>,
|
||||
pub domicile: Option<String>,
|
||||
pub phone_for_verification: Option<String>,
|
||||
pub bio: Option<String>,
|
||||
pub last_education: Option<String>,
|
||||
pub linkedin_url: Option<String>,
|
||||
pub github_url: Option<String>,
|
||||
pub cv_url: Option<String>,
|
||||
pub portfolio_url: Option<String>,
|
||||
pub industries: Vec<String>,
|
||||
pub expertise: Vec<String>,
|
||||
pub languages: Vec<String>,
|
||||
pub current_company: String,
|
||||
pub current_role: String,
|
||||
pub years_of_experience: i32,
|
||||
pub topics_of_interest: Vec<String>,
|
||||
pub preferred_mentee_level: Vec<String>,
|
||||
pub preferred_mentoring_formats: Vec<String>,
|
||||
pub availability_commitment: String,
|
||||
pub mentoring_rate: f64,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
pub struct MentorRegistered {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
pub email: Option<String>,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
pub struct MentorRegisterCommand {
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
pub fullname: String,
|
||||
pub phone_number: Option<String>,
|
||||
pub legal_name: String,
|
||||
pub gender: Option<String>,
|
||||
pub domicile: Option<String>,
|
||||
pub identity_document_url: String,
|
||||
pub phone_for_verification: Option<String>,
|
||||
pub bio: String,
|
||||
pub last_education: Option<String>,
|
||||
pub linkedin_url: Option<String>,
|
||||
pub github_url: Option<String>,
|
||||
pub cv_url: Option<String>,
|
||||
pub portfolio_url: Option<String>,
|
||||
pub industries: Vec<String>,
|
||||
pub expertise: Vec<String>,
|
||||
pub languages: Vec<String>,
|
||||
pub current_company: String,
|
||||
pub current_role: String,
|
||||
pub years_of_experience: i32,
|
||||
pub topics_of_interest: Vec<String>,
|
||||
pub preferred_mentee_level: Vec<String>,
|
||||
pub preferred_mentoring_formats: Vec<String>,
|
||||
pub availability_commitment: String,
|
||||
pub mentoring_rate_amount: u64,
|
||||
}
|
||||
|
||||
pub struct MentorUpdateCommand {
|
||||
pub legal_name: Option<String>,
|
||||
pub gender: Option<String>,
|
||||
pub domicile: Option<String>,
|
||||
pub phone_for_verification: Option<String>,
|
||||
pub bio: Option<String>,
|
||||
pub last_education: Option<String>,
|
||||
pub linkedin_url: Option<String>,
|
||||
pub github_url: Option<String>,
|
||||
pub cv_url: Option<String>,
|
||||
pub portfolio_url: Option<String>,
|
||||
pub industries: Option<Vec<String>>,
|
||||
pub expertise: Option<Vec<String>>,
|
||||
pub languages: Option<Vec<String>>,
|
||||
pub current_company: Option<String>,
|
||||
pub current_role: Option<String>,
|
||||
pub years_of_experience: Option<i32>,
|
||||
pub topics_of_interest: Option<Vec<String>>,
|
||||
pub preferred_mentee_level: Option<Vec<String>>,
|
||||
pub preferred_mentoring_formats: Option<Vec<String>>,
|
||||
pub availability_commitment: Option<String>,
|
||||
pub mentoring_rate_amount: Option<u64>,
|
||||
}
|
||||
|
||||
pub struct MentorVerifyCommand {
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
pub type MentorListPage = PaginatorResponse<MentorListItem>;
|
||||
@@ -1,7 +1,12 @@
|
||||
pub mod mentor;
|
||||
pub mod mentor_types;
|
||||
pub mod repository;
|
||||
pub mod service;
|
||||
|
||||
pub use mentor::MentorEntity;
|
||||
pub use mentor_types::{
|
||||
MentorDetail, MentorListItem, MentorListPage, MentorRegisterCommand,
|
||||
MentorRegistered, MentorUpdateCommand, MentorVerifyCommand,
|
||||
};
|
||||
pub use repository::MentorRepository;
|
||||
pub use service::MentorService;
|
||||
|
||||
@@ -1,33 +1,32 @@
|
||||
use super::mentor::MentorEntity;
|
||||
use async_trait::async_trait;
|
||||
use imphnen_utils::AppError;
|
||||
use paginator_rs::PaginationParams;
|
||||
use paginator_utils::PaginatorResponse;
|
||||
use uuid::Uuid;
|
||||
use imphnen_utils::AppError;
|
||||
use super::mentor::MentorEntity;
|
||||
|
||||
|
||||
#[async_trait]
|
||||
pub trait MentorRepository: Send + Sync {
|
||||
async fn find_all(
|
||||
&self,
|
||||
params: PaginationParams,
|
||||
) -> Result<PaginatorResponse<MentorEntity>, AppError>;
|
||||
async fn find_all(
|
||||
&self,
|
||||
params: PaginationParams,
|
||||
) -> Result<PaginatorResponse<MentorEntity>, AppError>;
|
||||
|
||||
async fn find_by_id(
|
||||
&self,
|
||||
id: Uuid,
|
||||
include_deleted: bool,
|
||||
) -> Result<MentorEntity, AppError>;
|
||||
async fn find_by_id(
|
||||
&self,
|
||||
id: Uuid,
|
||||
include_deleted: bool,
|
||||
) -> Result<MentorEntity, AppError>;
|
||||
|
||||
async fn find_by_user_id(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
include_deleted: bool,
|
||||
) -> Result<MentorEntity, AppError>;
|
||||
async fn find_by_user_id(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
include_deleted: bool,
|
||||
) -> Result<MentorEntity, AppError>;
|
||||
|
||||
async fn create(&self, entity: MentorEntity) -> Result<Uuid, AppError>;
|
||||
async fn create(&self, entity: MentorEntity) -> Result<Uuid, AppError>;
|
||||
|
||||
async fn update(&self, entity: MentorEntity) -> Result<(), AppError>;
|
||||
async fn update(&self, entity: MentorEntity) -> Result<(), AppError>;
|
||||
|
||||
async fn soft_delete(&self, id: Uuid) -> Result<(), AppError>;
|
||||
async fn soft_delete(&self, id: Uuid) -> Result<(), AppError>;
|
||||
}
|
||||
|
||||
@@ -1,55 +1,52 @@
|
||||
use async_trait::async_trait;
|
||||
use paginator_rs::PaginationParams;
|
||||
use paginator_utils::PaginatorResponse;
|
||||
use uuid::Uuid;
|
||||
use imphnen_utils::AppError;
|
||||
use super::mentor::MentorEntity;
|
||||
use crate::mentors::infrastructure::http::dto::{
|
||||
MentorDetailResponseDto, MentorListResponseDto, MentorRegisterResponseDto,
|
||||
MentorUpdateRequestDto, MentorUserRegisterRequestDto, MentorVerifyRequestDto,
|
||||
use super::mentor_types::{
|
||||
MentorDetail, MentorListPage, MentorRegisterCommand, MentorRegistered,
|
||||
MentorUpdateCommand, MentorVerifyCommand,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use imphnen_utils::AppError;
|
||||
use paginator_rs::PaginationParams;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[async_trait]
|
||||
pub trait MentorService: Send + Sync {
|
||||
async fn list(
|
||||
&self,
|
||||
params: PaginationParams,
|
||||
) -> Result<PaginatorResponse<MentorListResponseDto>, AppError>;
|
||||
async fn list(&self, params: PaginationParams)
|
||||
-> Result<MentorListPage, AppError>;
|
||||
|
||||
async fn get_by_id(&self, id: Uuid) -> Result<MentorDetailResponseDto, AppError>;
|
||||
async fn get_by_id(&self, id: Uuid) -> Result<MentorDetail, AppError>;
|
||||
|
||||
async fn get_by_email(&self, email: &str) -> Result<MentorDetailResponseDto, AppError>;
|
||||
async fn get_by_email(&self, email: &str) -> Result<MentorDetail, AppError>;
|
||||
|
||||
async fn register(
|
||||
&self,
|
||||
dto: MentorUserRegisterRequestDto,
|
||||
) -> Result<MentorRegisterResponseDto, AppError>;
|
||||
async fn register(
|
||||
&self,
|
||||
cmd: MentorRegisterCommand,
|
||||
) -> Result<MentorRegistered, AppError>;
|
||||
|
||||
async fn update(
|
||||
&self,
|
||||
id: Uuid,
|
||||
dto: MentorUpdateRequestDto,
|
||||
) -> Result<MentorDetailResponseDto, AppError>;
|
||||
async fn update(
|
||||
&self,
|
||||
id: Uuid,
|
||||
cmd: MentorUpdateCommand,
|
||||
) -> Result<MentorDetail, AppError>;
|
||||
|
||||
async fn update_me(
|
||||
&self,
|
||||
email: &str,
|
||||
dto: MentorUpdateRequestDto,
|
||||
) -> Result<MentorDetailResponseDto, AppError>;
|
||||
async fn update_me(
|
||||
&self,
|
||||
email: &str,
|
||||
cmd: MentorUpdateCommand,
|
||||
) -> Result<MentorDetail, AppError>;
|
||||
|
||||
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
|
||||
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
|
||||
|
||||
async fn verify(
|
||||
&self,
|
||||
id: Uuid,
|
||||
dto: MentorVerifyRequestDto,
|
||||
) -> Result<MentorDetailResponseDto, AppError>;
|
||||
async fn verify(
|
||||
&self,
|
||||
id: Uuid,
|
||||
cmd: MentorVerifyCommand,
|
||||
) -> Result<MentorDetail, AppError>;
|
||||
|
||||
async fn get_status(&self, email: &str) -> Result<String, AppError>;
|
||||
async fn get_status(&self, email: &str) -> Result<String, AppError>;
|
||||
|
||||
async fn get_entity_by_id(
|
||||
&self,
|
||||
id: Uuid,
|
||||
include_deleted: bool,
|
||||
) -> Result<MentorEntity, AppError>;
|
||||
async fn get_entity_by_id(
|
||||
&self,
|
||||
id: Uuid,
|
||||
include_deleted: bool,
|
||||
) -> Result<MentorEntity, AppError>;
|
||||
}
|
||||
|
||||
@@ -1,261 +0,0 @@
|
||||
use imphnen_libs::ZodValidate;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use zod_rs::prelude::*;
|
||||
|
||||
// ============================================================
|
||||
// Response DTOs
|
||||
// ============================================================
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct MentorListResponseDto {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
pub fullname: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct MentorDetailResponseDto {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
pub fullname: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub legal_name: Option<String>,
|
||||
pub gender: Option<String>,
|
||||
pub domicile: Option<String>,
|
||||
pub phone_for_verification: Option<String>,
|
||||
pub bio: Option<String>,
|
||||
pub last_education: Option<String>,
|
||||
pub linkedin_url: Option<String>,
|
||||
pub github_url: Option<String>,
|
||||
pub cv_url: Option<String>,
|
||||
pub portfolio_url: Option<String>,
|
||||
pub industries: Vec<String>,
|
||||
pub expertise: Vec<String>,
|
||||
pub languages: Vec<String>,
|
||||
pub current_company: String,
|
||||
pub current_role: String,
|
||||
pub years_of_experience: i32,
|
||||
pub topics_of_interest: Vec<String>,
|
||||
pub preferred_mentee_level: Vec<String>,
|
||||
pub preferred_mentoring_formats: Vec<String>,
|
||||
pub availability_commitment: String,
|
||||
pub mentoring_rate: f64,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct MentorRegisterResponseDto {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
pub email: Option<String>,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Request DTOs
|
||||
// ============================================================
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
pub struct MentorUserRegisterRequestDto {
|
||||
#[zod(email, min_length(1))]
|
||||
pub email: String,
|
||||
#[zod(min_length(8), regex(pattern = "^[A-Za-z\\d@$!%*?&]{8,}$"))]
|
||||
pub password: String,
|
||||
#[zod(min_length(2))]
|
||||
pub fullname: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub phone_number: Option<String>,
|
||||
pub identity_and_verification: IdentityAndVerification,
|
||||
pub professional_profile: ProfessionalProfile,
|
||||
pub mentoring_logistics: MentoringLogistics,
|
||||
}
|
||||
|
||||
impl ZodValidate for MentorUserRegisterRequestDto {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
pub struct IdentityAndVerification {
|
||||
#[zod(min_length(3))]
|
||||
pub legal_name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub gender: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub domicile: Option<String>,
|
||||
#[zod(url)]
|
||||
pub identity_document_url: String,
|
||||
#[zod(min_length(10), max_length(15))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub phone_for_verification: Option<String>,
|
||||
}
|
||||
|
||||
impl ZodValidate for IdentityAndVerification {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
pub struct ProfessionalProfile {
|
||||
#[zod(min_length(50))]
|
||||
pub bio: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub last_education: Option<String>,
|
||||
#[zod(url)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub linkedin_url: Option<String>,
|
||||
#[zod(url)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub github_url: Option<String>,
|
||||
#[zod(url)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cv_url: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub portfolio_url: Option<String>,
|
||||
pub industries: Vec<String>,
|
||||
pub expertise: Vec<String>,
|
||||
pub languages: Vec<String>,
|
||||
#[zod(min_length(1))]
|
||||
pub current_company: String,
|
||||
#[zod(min_length(1))]
|
||||
pub current_role: String,
|
||||
#[zod(min(2.0), int)]
|
||||
pub years_of_experience: i32,
|
||||
}
|
||||
|
||||
impl ZodValidate for ProfessionalProfile {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
pub struct MentoringLogistics {
|
||||
pub topics_of_interest: Vec<String>,
|
||||
pub preferred_mentee_level: Vec<String>,
|
||||
pub preferred_mentoring_formats: Vec<String>,
|
||||
#[zod(min_length(5))]
|
||||
pub availability_commitment: String,
|
||||
#[zod(min(1.0))]
|
||||
pub mentoring_rate_amount: u64,
|
||||
}
|
||||
|
||||
impl ZodValidate for MentoringLogistics {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
pub struct MentorUpdateRequestDto {
|
||||
#[zod(min_length(3))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub legal_name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub gender: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub domicile: Option<String>,
|
||||
#[zod(min_length(10), max_length(15))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub phone_for_verification: Option<String>,
|
||||
#[zod(min_length(50))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub bio: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub last_education: Option<String>,
|
||||
#[zod(url)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub linkedin_url: Option<String>,
|
||||
#[zod(url)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub github_url: Option<String>,
|
||||
#[zod(url)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cv_url: Option<String>,
|
||||
#[zod(url)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub portfolio_url: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub industries: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub expertise: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub languages: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub current_company: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub current_role: Option<String>,
|
||||
#[zod(min(2.0), int)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub years_of_experience: Option<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub topics_of_interest: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub preferred_mentee_level: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub preferred_mentoring_formats: Option<Vec<String>>,
|
||||
#[zod(min_length(5))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub availability_commitment: Option<String>,
|
||||
#[zod(min(1.0))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mentoring_rate_amount: Option<u64>,
|
||||
}
|
||||
|
||||
impl ZodValidate for MentorUpdateRequestDto {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
pub struct MentorVerifyRequestDto {
|
||||
#[zod(min_length(1))]
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
impl ZodValidate for MentorVerifyRequestDto {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema, Default)]
|
||||
pub struct MentoringRate {
|
||||
#[zod(min(1.0))]
|
||||
pub amount: u64,
|
||||
#[zod(min_length(1))]
|
||||
pub currency: String,
|
||||
#[zod(min_length(1))]
|
||||
pub per_duration: String,
|
||||
}
|
||||
|
||||
impl ZodValidate for MentoringRate {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
pub struct MentorRegisterFromTokenRequestDto {
|
||||
pub identity_and_verification: IdentityAndVerification,
|
||||
pub professional_profile: ProfessionalProfile,
|
||||
pub mentoring_logistics: MentoringLogistics,
|
||||
}
|
||||
|
||||
impl ZodValidate for MentorRegisterFromTokenRequestDto {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
pub mod nested;
|
||||
pub mod request;
|
||||
pub mod response;
|
||||
|
||||
pub use nested::{
|
||||
IdentityAndVerification, MentoringLogistics, MentoringRate, ProfessionalProfile,
|
||||
};
|
||||
pub use request::{
|
||||
MentorRegisterFromTokenRequestDto, MentorUpdateRequestDto,
|
||||
MentorUserRegisterRequestDto, MentorVerifyRequestDto,
|
||||
};
|
||||
pub use response::{
|
||||
MentorDetailResponseDto, MentorListResponseDto, MentorRegisterResponseDto,
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
use imphnen_libs::ZodValidate;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use zod_rs::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
pub struct IdentityAndVerification {
|
||||
#[zod(min_length(3))]
|
||||
pub legal_name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub gender: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub domicile: Option<String>,
|
||||
#[zod(url)]
|
||||
pub identity_document_url: String,
|
||||
#[zod(min_length(10), max_length(15))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub phone_for_verification: Option<String>,
|
||||
}
|
||||
|
||||
impl ZodValidate for IdentityAndVerification {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
pub struct ProfessionalProfile {
|
||||
#[zod(min_length(50))]
|
||||
pub bio: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub last_education: Option<String>,
|
||||
#[zod(url)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub linkedin_url: Option<String>,
|
||||
#[zod(url)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub github_url: Option<String>,
|
||||
#[zod(url)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cv_url: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub portfolio_url: Option<String>,
|
||||
pub industries: Vec<String>,
|
||||
pub expertise: Vec<String>,
|
||||
pub languages: Vec<String>,
|
||||
#[zod(min_length(1))]
|
||||
pub current_company: String,
|
||||
#[zod(min_length(1))]
|
||||
pub current_role: String,
|
||||
#[zod(min(2.0), int)]
|
||||
pub years_of_experience: i32,
|
||||
}
|
||||
|
||||
impl ZodValidate for ProfessionalProfile {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
pub struct MentoringLogistics {
|
||||
pub topics_of_interest: Vec<String>,
|
||||
pub preferred_mentee_level: Vec<String>,
|
||||
pub preferred_mentoring_formats: Vec<String>,
|
||||
#[zod(min_length(5))]
|
||||
pub availability_commitment: String,
|
||||
#[zod(min(1.0))]
|
||||
pub mentoring_rate_amount: u64,
|
||||
}
|
||||
|
||||
impl ZodValidate for MentoringLogistics {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema, Default)]
|
||||
pub struct MentoringRate {
|
||||
#[zod(min(1.0))]
|
||||
pub amount: u64,
|
||||
#[zod(min_length(1))]
|
||||
pub currency: String,
|
||||
#[zod(min_length(1))]
|
||||
pub per_duration: String,
|
||||
}
|
||||
|
||||
impl ZodValidate for MentoringRate {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
use super::nested::{
|
||||
IdentityAndVerification, MentoringLogistics, ProfessionalProfile,
|
||||
};
|
||||
use crate::mentors::domain::{
|
||||
MentorRegisterCommand, MentorUpdateCommand, MentorVerifyCommand,
|
||||
};
|
||||
use imphnen_libs::ZodValidate;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use zod_rs::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
pub struct MentorUserRegisterRequestDto {
|
||||
#[zod(email, min_length(1))]
|
||||
pub email: String,
|
||||
#[zod(min_length(8), regex(pattern = "^[A-Za-z\\d@$!%*?&]{8,}$"))]
|
||||
pub password: String,
|
||||
#[zod(min_length(2))]
|
||||
pub fullname: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub phone_number: Option<String>,
|
||||
pub identity_and_verification: IdentityAndVerification,
|
||||
pub professional_profile: ProfessionalProfile,
|
||||
pub mentoring_logistics: MentoringLogistics,
|
||||
}
|
||||
|
||||
impl ZodValidate for MentorUserRegisterRequestDto {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MentorUserRegisterRequestDto> for MentorRegisterCommand {
|
||||
fn from(dto: MentorUserRegisterRequestDto) -> Self {
|
||||
Self {
|
||||
email: dto.email,
|
||||
password: dto.password,
|
||||
fullname: dto.fullname,
|
||||
phone_number: dto.phone_number,
|
||||
legal_name: dto.identity_and_verification.legal_name,
|
||||
gender: dto.identity_and_verification.gender,
|
||||
domicile: dto.identity_and_verification.domicile,
|
||||
identity_document_url: dto.identity_and_verification.identity_document_url,
|
||||
phone_for_verification: dto.identity_and_verification.phone_for_verification,
|
||||
bio: dto.professional_profile.bio,
|
||||
last_education: dto.professional_profile.last_education,
|
||||
linkedin_url: dto.professional_profile.linkedin_url,
|
||||
github_url: dto.professional_profile.github_url,
|
||||
cv_url: dto.professional_profile.cv_url,
|
||||
portfolio_url: dto.professional_profile.portfolio_url,
|
||||
industries: dto.professional_profile.industries,
|
||||
expertise: dto.professional_profile.expertise,
|
||||
languages: dto.professional_profile.languages,
|
||||
current_company: dto.professional_profile.current_company,
|
||||
current_role: dto.professional_profile.current_role,
|
||||
years_of_experience: dto.professional_profile.years_of_experience,
|
||||
topics_of_interest: dto.mentoring_logistics.topics_of_interest,
|
||||
preferred_mentee_level: dto.mentoring_logistics.preferred_mentee_level,
|
||||
preferred_mentoring_formats: dto
|
||||
.mentoring_logistics
|
||||
.preferred_mentoring_formats,
|
||||
availability_commitment: dto.mentoring_logistics.availability_commitment,
|
||||
mentoring_rate_amount: dto.mentoring_logistics.mentoring_rate_amount,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
pub struct MentorUpdateRequestDto {
|
||||
#[zod(min_length(3))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub legal_name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub gender: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub domicile: Option<String>,
|
||||
#[zod(min_length(10), max_length(15))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub phone_for_verification: Option<String>,
|
||||
#[zod(min_length(50))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub bio: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub last_education: Option<String>,
|
||||
#[zod(url)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub linkedin_url: Option<String>,
|
||||
#[zod(url)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub github_url: Option<String>,
|
||||
#[zod(url)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cv_url: Option<String>,
|
||||
#[zod(url)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub portfolio_url: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub industries: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub expertise: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub languages: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub current_company: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub current_role: Option<String>,
|
||||
#[zod(min(2.0), int)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub years_of_experience: Option<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub topics_of_interest: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub preferred_mentee_level: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub preferred_mentoring_formats: Option<Vec<String>>,
|
||||
#[zod(min_length(5))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub availability_commitment: Option<String>,
|
||||
#[zod(min(1.0))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mentoring_rate_amount: Option<u64>,
|
||||
}
|
||||
|
||||
impl ZodValidate for MentorUpdateRequestDto {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MentorUpdateRequestDto> for MentorUpdateCommand {
|
||||
fn from(dto: MentorUpdateRequestDto) -> Self {
|
||||
Self {
|
||||
legal_name: dto.legal_name,
|
||||
gender: dto.gender,
|
||||
domicile: dto.domicile,
|
||||
phone_for_verification: dto.phone_for_verification,
|
||||
bio: dto.bio,
|
||||
last_education: dto.last_education,
|
||||
linkedin_url: dto.linkedin_url,
|
||||
github_url: dto.github_url,
|
||||
cv_url: dto.cv_url,
|
||||
portfolio_url: dto.portfolio_url,
|
||||
industries: dto.industries,
|
||||
expertise: dto.expertise,
|
||||
languages: dto.languages,
|
||||
current_company: dto.current_company,
|
||||
current_role: dto.current_role,
|
||||
years_of_experience: dto.years_of_experience,
|
||||
topics_of_interest: dto.topics_of_interest,
|
||||
preferred_mentee_level: dto.preferred_mentee_level,
|
||||
preferred_mentoring_formats: dto.preferred_mentoring_formats,
|
||||
availability_commitment: dto.availability_commitment,
|
||||
mentoring_rate_amount: dto.mentoring_rate_amount,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
pub struct MentorVerifyRequestDto {
|
||||
#[zod(min_length(1))]
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
impl ZodValidate for MentorVerifyRequestDto {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MentorVerifyRequestDto> for MentorVerifyCommand {
|
||||
fn from(dto: MentorVerifyRequestDto) -> Self {
|
||||
Self { status: dto.status }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
pub struct MentorRegisterFromTokenRequestDto {
|
||||
pub identity_and_verification: IdentityAndVerification,
|
||||
pub professional_profile: ProfessionalProfile,
|
||||
pub mentoring_logistics: MentoringLogistics,
|
||||
}
|
||||
|
||||
impl ZodValidate for MentorRegisterFromTokenRequestDto {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
use crate::mentors::domain::{MentorDetail, MentorListItem, MentorRegistered};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct MentorListResponseDto {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
pub fullname: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl From<MentorListItem> for MentorListResponseDto {
|
||||
fn from(item: MentorListItem) -> Self {
|
||||
Self {
|
||||
id: item.id,
|
||||
user_id: item.user_id,
|
||||
fullname: item.fullname,
|
||||
email: item.email,
|
||||
status: item.status,
|
||||
created_at: item.created_at,
|
||||
updated_at: item.updated_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct MentorDetailResponseDto {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
pub fullname: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub legal_name: Option<String>,
|
||||
pub gender: Option<String>,
|
||||
pub domicile: Option<String>,
|
||||
pub phone_for_verification: Option<String>,
|
||||
pub bio: Option<String>,
|
||||
pub last_education: Option<String>,
|
||||
pub linkedin_url: Option<String>,
|
||||
pub github_url: Option<String>,
|
||||
pub cv_url: Option<String>,
|
||||
pub portfolio_url: Option<String>,
|
||||
pub industries: Vec<String>,
|
||||
pub expertise: Vec<String>,
|
||||
pub languages: Vec<String>,
|
||||
pub current_company: String,
|
||||
pub current_role: String,
|
||||
pub years_of_experience: i32,
|
||||
pub topics_of_interest: Vec<String>,
|
||||
pub preferred_mentee_level: Vec<String>,
|
||||
pub preferred_mentoring_formats: Vec<String>,
|
||||
pub availability_commitment: String,
|
||||
pub mentoring_rate: f64,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl From<MentorDetail> for MentorDetailResponseDto {
|
||||
fn from(d: MentorDetail) -> Self {
|
||||
Self {
|
||||
id: d.id,
|
||||
user_id: d.user_id,
|
||||
fullname: d.fullname,
|
||||
email: d.email,
|
||||
legal_name: d.legal_name,
|
||||
gender: d.gender,
|
||||
domicile: d.domicile,
|
||||
phone_for_verification: d.phone_for_verification,
|
||||
bio: d.bio,
|
||||
last_education: d.last_education,
|
||||
linkedin_url: d.linkedin_url,
|
||||
github_url: d.github_url,
|
||||
cv_url: d.cv_url,
|
||||
portfolio_url: d.portfolio_url,
|
||||
industries: d.industries,
|
||||
expertise: d.expertise,
|
||||
languages: d.languages,
|
||||
current_company: d.current_company,
|
||||
current_role: d.current_role,
|
||||
years_of_experience: d.years_of_experience,
|
||||
topics_of_interest: d.topics_of_interest,
|
||||
preferred_mentee_level: d.preferred_mentee_level,
|
||||
preferred_mentoring_formats: d.preferred_mentoring_formats,
|
||||
availability_commitment: d.availability_commitment,
|
||||
mentoring_rate: d.mentoring_rate,
|
||||
status: d.status,
|
||||
created_at: d.created_at,
|
||||
updated_at: d.updated_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct MentorRegisterResponseDto {
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
pub email: Option<String>,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl From<MentorRegistered> for MentorRegisterResponseDto {
|
||||
fn from(r: MentorRegistered) -> Self {
|
||||
Self {
|
||||
id: r.id,
|
||||
user_id: r.user_id,
|
||||
email: r.email,
|
||||
status: r.status,
|
||||
created_at: r.created_at,
|
||||
updated_at: r.updated_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,279 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
use axum::{
|
||||
extract::{Extension, Path},
|
||||
http::HeaderMap,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use paginator_axum::PaginationQuery;
|
||||
use uuid::Uuid;
|
||||
use imphnen_libs::{AppState, ValidatedJson};
|
||||
use imphnen_utils::{ApiSuccess, ApiPaginated, ApiMessage, extract_email};
|
||||
use imphnen_iam::{PermissionsEnum, require_permissions};
|
||||
use imphnen_utils::AppError;
|
||||
use crate::mentors::domain::MentorService;
|
||||
use super::dto::{
|
||||
MentorDetailResponseDto, MentorListResponseDto, MentorRegisterResponseDto,
|
||||
MentorUpdateRequestDto, MentorUserRegisterRequestDto, MentorVerifyRequestDto,
|
||||
};
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/mentors/create",
|
||||
request_body = MentorUserRegisterRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "[PUBLIC] Mentor registered successfully", body = MentorRegisterResponseDto),
|
||||
(status = 400, description = "[PUBLIC] Bad request - validation error"),
|
||||
(status = 409, description = "[PUBLIC] Conflict - user already has mentor profile"),
|
||||
(status = 500, description = "[PUBLIC] Internal server error")
|
||||
),
|
||||
tag = "Mentors"
|
||||
)]
|
||||
pub async fn post_register_mentor(
|
||||
Extension(service): Extension<Arc<dyn MentorService>>,
|
||||
ValidatedJson(dto): ValidatedJson<MentorUserRegisterRequestDto>,
|
||||
) -> Response {
|
||||
match service.register(dto).await {
|
||||
Ok(resp) => ApiSuccess(resp).into_response(),
|
||||
Err(e) => ApiMessage::new(e.status_code(), e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/mentors",
|
||||
params(
|
||||
("page" = Option<u64>, Query, description = "Page number"),
|
||||
("per_page" = Option<u64>, Query, description = "Items per page"),
|
||||
("search" = Option<String>, Query, description = "Search query"),
|
||||
("sort_by" = Option<String>, Query, description = "Sort by field"),
|
||||
("order" = Option<String>, Query, description = "Sort order (ASC/DESC)"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "[ADMIN] Get list of mentors", body = Vec<MentorListResponseDto>),
|
||||
(status = 500, description = "[ADMIN] Internal server error")
|
||||
),
|
||||
tag = "Mentors",
|
||||
security(("Bearer" = []))
|
||||
)]
|
||||
pub async fn get_mentor_list(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(service): Extension<Arc<dyn MentorService>>,
|
||||
PaginationQuery(params): PaginationQuery,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
require_permissions!(headers, state, [PermissionsEnum::ReadListMentors], {
|
||||
let result = service.list(params).await?;
|
||||
Ok(ApiPaginated(result))
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/mentors/detail/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Mentor ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "[ADMIN] Get mentor by ID", body = MentorDetailResponseDto),
|
||||
(status = 404, description = "[ADMIN] Mentor not found"),
|
||||
(status = 500, description = "[ADMIN] Internal server error")
|
||||
),
|
||||
tag = "Mentors",
|
||||
security(("Bearer" = []))
|
||||
)]
|
||||
pub async fn get_mentor_by_id(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(service): Extension<Arc<dyn MentorService>>,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let mentor_uuid = Uuid::parse_str(&id)
|
||||
.map_err(|_| AppError::BadRequestError("Invalid mentor ID format. Must be a valid UUID.".to_string()))?;
|
||||
require_permissions!(headers, state, [PermissionsEnum::ReadDetailMentors], {
|
||||
let dto = service.get_by_id(mentor_uuid).await?;
|
||||
Ok(ApiSuccess(dto))
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/v1/mentors/update/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Mentor ID")
|
||||
),
|
||||
request_body = MentorUpdateRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "[ADMIN] Mentor updated successfully", body = MentorDetailResponseDto),
|
||||
(status = 400, description = "[ADMIN] Bad request - validation error"),
|
||||
(status = 404, description = "[ADMIN] Mentor not found"),
|
||||
(status = 500, description = "[ADMIN] Internal server error")
|
||||
),
|
||||
tag = "Mentors - Admin",
|
||||
security(("Bearer" = []))
|
||||
)]
|
||||
pub async fn put_update_mentor(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(service): Extension<Arc<dyn MentorService>>,
|
||||
Path(id): Path<String>,
|
||||
ValidatedJson(dto): ValidatedJson<MentorUpdateRequestDto>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let mentor_uuid = Uuid::parse_str(&id)
|
||||
.map_err(|_| AppError::BadRequestError("Invalid mentor ID format. Must be a valid UUID.".to_string()))?;
|
||||
require_permissions!(headers, state, [PermissionsEnum::UpdateMentors], {
|
||||
let result = service.update(mentor_uuid, dto).await?;
|
||||
Ok(ApiSuccess(result))
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/v1/mentors/delete/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Mentor ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "[ADMIN] Mentor deleted successfully"),
|
||||
(status = 404, description = "[ADMIN] Mentor not found"),
|
||||
(status = 500, description = "[ADMIN] Internal server error")
|
||||
),
|
||||
tag = "Mentors - Admin",
|
||||
security(("Bearer" = []))
|
||||
)]
|
||||
pub async fn delete_mentor(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(service): Extension<Arc<dyn MentorService>>,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let mentor_uuid = Uuid::parse_str(&id)
|
||||
.map_err(|_| AppError::BadRequestError("Invalid mentor ID format. Must be a valid UUID.".to_string()))?;
|
||||
require_permissions!(headers, state, [PermissionsEnum::DeleteMentors], {
|
||||
service.delete(mentor_uuid).await?;
|
||||
Ok(ApiMessage::ok("Mentor deleted successfully"))
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/v1/mentors/verify/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Mentor ID")
|
||||
),
|
||||
request_body = MentorVerifyRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "[ADMIN] Mentor verified successfully", body = MentorDetailResponseDto),
|
||||
(status = 400, description = "[ADMIN] Bad request - validation error"),
|
||||
(status = 404, description = "[ADMIN] Mentor not found"),
|
||||
(status = 500, description = "[ADMIN] Internal server error")
|
||||
),
|
||||
tag = "Mentors - Admin",
|
||||
security(("Bearer" = []))
|
||||
)]
|
||||
pub async fn put_verify_mentor(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(service): Extension<Arc<dyn MentorService>>,
|
||||
Path(id): Path<String>,
|
||||
ValidatedJson(dto): ValidatedJson<MentorVerifyRequestDto>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let mentor_uuid = Uuid::parse_str(&id)
|
||||
.map_err(|_| AppError::BadRequestError("Invalid mentor ID format. Must be a valid UUID.".to_string()))?;
|
||||
require_permissions!(headers, state, [PermissionsEnum::VerifyMentors], {
|
||||
let result = service.verify(mentor_uuid, dto).await?;
|
||||
Ok(ApiSuccess(result))
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/mentors/me",
|
||||
responses(
|
||||
(status = 200, description = "[MENTOR] Current user's mentor profile", body = MentorDetailResponseDto),
|
||||
(status = 401, description = "[MENTOR] Unauthorized - invalid token"),
|
||||
(status = 403, description = "[MENTOR] Mentor profile not found for current user"),
|
||||
(status = 500, description = "[MENTOR] Internal server error")
|
||||
),
|
||||
tag = "Mentors",
|
||||
security(("Bearer" = []))
|
||||
)]
|
||||
pub async fn get_mentor_me(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(service): Extension<Arc<dyn MentorService>>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
require_permissions!(headers.clone(), state, [PermissionsEnum::ReadOwnMentorProfile], {
|
||||
let email = extract_email(&headers)
|
||||
.ok_or_else(|| AppError::AuthenticationError("Token tidak valid".to_string()))?;
|
||||
let dto = service.get_by_email(&email).await
|
||||
.map_err(|_| AppError::ForbiddenError("Mentor profile not found for current user".to_string()))?;
|
||||
Ok(ApiSuccess(dto))
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/v1/mentors/me/update",
|
||||
request_body = MentorUpdateRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "[MENTOR] Mentor profile updated successfully", body = MentorDetailResponseDto),
|
||||
(status = 400, description = "[MENTOR] Bad request - validation error"),
|
||||
(status = 401, description = "[MENTOR] Unauthorized - invalid token"),
|
||||
(status = 404, description = "[MENTOR] Mentor profile not found"),
|
||||
(status = 500, description = "[MENTOR] Internal server error")
|
||||
),
|
||||
tag = "Mentors",
|
||||
security(("Bearer" = []))
|
||||
)]
|
||||
pub async fn put_update_mentor_me(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(service): Extension<Arc<dyn MentorService>>,
|
||||
ValidatedJson(dto): ValidatedJson<MentorUpdateRequestDto>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
require_permissions!(headers.clone(), state, [PermissionsEnum::UpdateOwnMentorProfile], {
|
||||
let email = extract_email(&headers)
|
||||
.ok_or_else(|| AppError::AuthenticationError("Token tidak valid".to_string()))?;
|
||||
let resp = service.update_me(&email, dto).await?;
|
||||
Ok(ApiSuccess(resp))
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/v1/mentors/update",
|
||||
request_body = MentorUpdateRequestDto,
|
||||
responses(
|
||||
(status = 400, description = "[PUBLIC] Bad request - Mentor ID is required for update"),
|
||||
),
|
||||
tag = "Mentors - Admin"
|
||||
)]
|
||||
pub async fn put_update_mentor_no_id() -> impl IntoResponse {
|
||||
ApiMessage::new(axum::http::StatusCode::BAD_REQUEST, "Mentor ID is required for update")
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/mentors/me/status",
|
||||
responses(
|
||||
(status = 200, description = "[MENTOR] Mentor application status", body = String),
|
||||
(status = 401, description = "[MENTOR] Unauthorized - invalid token"),
|
||||
(status = 403, description = "[MENTOR] No mentor application found for current user"),
|
||||
(status = 500, description = "[MENTOR] Internal server error")
|
||||
),
|
||||
tag = "Mentors",
|
||||
security(("Bearer" = []))
|
||||
)]
|
||||
pub async fn get_mentor_status(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(service): Extension<Arc<dyn MentorService>>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
require_permissions!(headers.clone(), state, [PermissionsEnum::ReadOwnMentorStatus], {
|
||||
let email = extract_email(&headers)
|
||||
.ok_or_else(|| AppError::AuthenticationError("Token tidak valid".to_string()))?;
|
||||
let status = service.get_status(&email).await
|
||||
.map_err(|_| AppError::ForbiddenError("No mentor application found for current user".to_string()))?;
|
||||
Ok(ApiMessage::ok(&status))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
pub mod mutation_handlers;
|
||||
pub mod query_handlers;
|
||||
|
||||
pub use mutation_handlers::{
|
||||
delete_mentor, post_register_mentor, put_update_mentor, put_update_mentor_me,
|
||||
put_update_mentor_no_id, put_verify_mentor,
|
||||
};
|
||||
pub use query_handlers::{
|
||||
get_mentor_by_id, get_mentor_list, get_mentor_me, get_mentor_status,
|
||||
};
|
||||
@@ -0,0 +1,192 @@
|
||||
use super::super::dto::{
|
||||
MentorDetailResponseDto, MentorRegisterResponseDto, MentorUpdateRequestDto,
|
||||
MentorUserRegisterRequestDto, MentorVerifyRequestDto,
|
||||
};
|
||||
use crate::mentors::domain::MentorService;
|
||||
use axum::{
|
||||
extract::{Extension, Path},
|
||||
http::HeaderMap,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use imphnen_iam::{PermissionsEnum, require_permissions};
|
||||
use imphnen_libs::{AppState, ValidatedJson};
|
||||
use imphnen_utils::AppError;
|
||||
use imphnen_utils::{ApiMessage, ApiSuccess, extract_email};
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/mentors/create",
|
||||
request_body = MentorUserRegisterRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "[PUBLIC] Mentor registered successfully", body = MentorRegisterResponseDto),
|
||||
(status = 400, description = "[PUBLIC] Bad request - validation error"),
|
||||
(status = 409, description = "[PUBLIC] Conflict - user already has mentor profile"),
|
||||
(status = 500, description = "[PUBLIC] Internal server error")
|
||||
),
|
||||
tag = "Mentors"
|
||||
)]
|
||||
pub async fn post_register_mentor(
|
||||
Extension(service): Extension<Arc<dyn MentorService>>,
|
||||
ValidatedJson(dto): ValidatedJson<MentorUserRegisterRequestDto>,
|
||||
) -> Response {
|
||||
match service.register(dto.into()).await {
|
||||
Ok(resp) => axum::response::IntoResponse::into_response(
|
||||
imphnen_utils::ApiSuccess(MentorRegisterResponseDto::from(resp)),
|
||||
),
|
||||
Err(e) => ApiMessage::new(e.status_code(), e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/v1/mentors/update/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Mentor ID")
|
||||
),
|
||||
request_body = MentorUpdateRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "[ADMIN] Mentor updated successfully", body = MentorDetailResponseDto),
|
||||
(status = 400, description = "[ADMIN] Bad request - validation error"),
|
||||
(status = 404, description = "[ADMIN] Mentor not found"),
|
||||
(status = 500, description = "[ADMIN] Internal server error")
|
||||
),
|
||||
tag = "Mentors - Admin",
|
||||
security(("Bearer" = []))
|
||||
)]
|
||||
pub async fn put_update_mentor(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(service): Extension<Arc<dyn MentorService>>,
|
||||
Path(id): Path<String>,
|
||||
ValidatedJson(dto): ValidatedJson<MentorUpdateRequestDto>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let mentor_uuid = Uuid::parse_str(&id).map_err(|_| {
|
||||
AppError::BadRequestError(
|
||||
"Invalid mentor ID format. Must be a valid UUID.".to_string(),
|
||||
)
|
||||
})?;
|
||||
require_permissions!(headers, state, [PermissionsEnum::UpdateMentors], {
|
||||
let result =
|
||||
MentorDetailResponseDto::from(service.update(mentor_uuid, dto.into()).await?);
|
||||
Ok(ApiSuccess(result))
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/v1/mentors/delete/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Mentor ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "[ADMIN] Mentor deleted successfully"),
|
||||
(status = 404, description = "[ADMIN] Mentor not found"),
|
||||
(status = 500, description = "[ADMIN] Internal server error")
|
||||
),
|
||||
tag = "Mentors - Admin",
|
||||
security(("Bearer" = []))
|
||||
)]
|
||||
pub async fn delete_mentor(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(service): Extension<Arc<dyn MentorService>>,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let mentor_uuid = Uuid::parse_str(&id).map_err(|_| {
|
||||
AppError::BadRequestError(
|
||||
"Invalid mentor ID format. Must be a valid UUID.".to_string(),
|
||||
)
|
||||
})?;
|
||||
require_permissions!(headers, state, [PermissionsEnum::DeleteMentors], {
|
||||
service.delete(mentor_uuid).await?;
|
||||
Ok(ApiMessage::ok("Mentor deleted successfully"))
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/v1/mentors/verify/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Mentor ID")
|
||||
),
|
||||
request_body = MentorVerifyRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "[ADMIN] Mentor verified successfully", body = MentorDetailResponseDto),
|
||||
(status = 400, description = "[ADMIN] Bad request - validation error"),
|
||||
(status = 404, description = "[ADMIN] Mentor not found"),
|
||||
(status = 500, description = "[ADMIN] Internal server error")
|
||||
),
|
||||
tag = "Mentors - Admin",
|
||||
security(("Bearer" = []))
|
||||
)]
|
||||
pub async fn put_verify_mentor(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(service): Extension<Arc<dyn MentorService>>,
|
||||
Path(id): Path<String>,
|
||||
ValidatedJson(dto): ValidatedJson<MentorVerifyRequestDto>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let mentor_uuid = Uuid::parse_str(&id).map_err(|_| {
|
||||
AppError::BadRequestError(
|
||||
"Invalid mentor ID format. Must be a valid UUID.".to_string(),
|
||||
)
|
||||
})?;
|
||||
require_permissions!(headers, state, [PermissionsEnum::VerifyMentors], {
|
||||
let result =
|
||||
MentorDetailResponseDto::from(service.verify(mentor_uuid, dto.into()).await?);
|
||||
Ok(ApiSuccess(result))
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/v1/mentors/me/update",
|
||||
request_body = MentorUpdateRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "[MENTOR] Mentor profile updated successfully", body = MentorDetailResponseDto),
|
||||
(status = 400, description = "[MENTOR] Bad request - validation error"),
|
||||
(status = 401, description = "[MENTOR] Unauthorized - invalid token"),
|
||||
(status = 404, description = "[MENTOR] Mentor profile not found"),
|
||||
(status = 500, description = "[MENTOR] Internal server error")
|
||||
),
|
||||
tag = "Mentors",
|
||||
security(("Bearer" = []))
|
||||
)]
|
||||
pub async fn put_update_mentor_me(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(service): Extension<Arc<dyn MentorService>>,
|
||||
ValidatedJson(dto): ValidatedJson<MentorUpdateRequestDto>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
require_permissions!(
|
||||
headers.clone(),
|
||||
state,
|
||||
[PermissionsEnum::UpdateOwnMentorProfile],
|
||||
{
|
||||
let email = extract_email(&headers).ok_or_else(|| {
|
||||
AppError::AuthenticationError("Token tidak valid".to_string())
|
||||
})?;
|
||||
let resp =
|
||||
MentorDetailResponseDto::from(service.update_me(&email, dto.into()).await?);
|
||||
Ok(ApiSuccess(resp))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/v1/mentors/update",
|
||||
request_body = MentorUpdateRequestDto,
|
||||
responses(
|
||||
(status = 400, description = "[PUBLIC] Bad request - Mentor ID is required for update"),
|
||||
),
|
||||
tag = "Mentors - Admin"
|
||||
)]
|
||||
pub async fn put_update_mentor_no_id() -> impl IntoResponse {
|
||||
ApiMessage::new(
|
||||
axum::http::StatusCode::BAD_REQUEST,
|
||||
"Mentor ID is required for update",
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
use super::super::dto::{MentorDetailResponseDto, MentorListResponseDto};
|
||||
use crate::mentors::domain::MentorService;
|
||||
use axum::{
|
||||
extract::{Extension, Path},
|
||||
http::HeaderMap,
|
||||
response::IntoResponse,
|
||||
};
|
||||
use imphnen_iam::{PermissionsEnum, require_permissions};
|
||||
use imphnen_libs::AppState;
|
||||
use imphnen_utils::AppError;
|
||||
use imphnen_utils::{ApiMessage, ApiPaginated, ApiSuccess, extract_email};
|
||||
use paginator_axum::PaginationQuery;
|
||||
use paginator_utils::PaginatorResponse;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/mentors",
|
||||
params(
|
||||
("page" = Option<u64>, Query, description = "Page number"),
|
||||
("per_page" = Option<u64>, Query, description = "Items per page"),
|
||||
("search" = Option<String>, Query, description = "Search query"),
|
||||
("sort_by" = Option<String>, Query, description = "Sort by field"),
|
||||
("order" = Option<String>, Query, description = "Sort order (ASC/DESC)"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "[ADMIN] Get list of mentors", body = Vec<MentorListResponseDto>),
|
||||
(status = 500, description = "[ADMIN] Internal server error")
|
||||
),
|
||||
tag = "Mentors",
|
||||
security(("Bearer" = []))
|
||||
)]
|
||||
pub async fn get_mentor_list(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(service): Extension<Arc<dyn MentorService>>,
|
||||
PaginationQuery(params): PaginationQuery,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
require_permissions!(headers, state, [PermissionsEnum::ReadListMentors], {
|
||||
let result = service.list(params).await?;
|
||||
let mapped = PaginatorResponse {
|
||||
data: result
|
||||
.data
|
||||
.into_iter()
|
||||
.map(MentorListResponseDto::from)
|
||||
.collect(),
|
||||
meta: result.meta,
|
||||
};
|
||||
Ok(ApiPaginated(mapped))
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/mentors/detail/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Mentor ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "[ADMIN] Get mentor by ID", body = MentorDetailResponseDto),
|
||||
(status = 404, description = "[ADMIN] Mentor not found"),
|
||||
(status = 500, description = "[ADMIN] Internal server error")
|
||||
),
|
||||
tag = "Mentors",
|
||||
security(("Bearer" = []))
|
||||
)]
|
||||
pub async fn get_mentor_by_id(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(service): Extension<Arc<dyn MentorService>>,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let mentor_uuid = Uuid::parse_str(&id).map_err(|_| {
|
||||
AppError::BadRequestError(
|
||||
"Invalid mentor ID format. Must be a valid UUID.".to_string(),
|
||||
)
|
||||
})?;
|
||||
require_permissions!(headers, state, [PermissionsEnum::ReadDetailMentors], {
|
||||
let dto = MentorDetailResponseDto::from(service.get_by_id(mentor_uuid).await?);
|
||||
Ok(ApiSuccess(dto))
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/mentors/me",
|
||||
responses(
|
||||
(status = 200, description = "[MENTOR] Current user's mentor profile", body = MentorDetailResponseDto),
|
||||
(status = 401, description = "[MENTOR] Unauthorized - invalid token"),
|
||||
(status = 403, description = "[MENTOR] Mentor profile not found for current user"),
|
||||
(status = 500, description = "[MENTOR] Internal server error")
|
||||
),
|
||||
tag = "Mentors",
|
||||
security(("Bearer" = []))
|
||||
)]
|
||||
pub async fn get_mentor_me(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(service): Extension<Arc<dyn MentorService>>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
require_permissions!(
|
||||
headers.clone(),
|
||||
state,
|
||||
[PermissionsEnum::ReadOwnMentorProfile],
|
||||
{
|
||||
let email = extract_email(&headers).ok_or_else(|| {
|
||||
AppError::AuthenticationError("Token tidak valid".to_string())
|
||||
})?;
|
||||
let detail = service.get_by_email(&email).await.map_err(|_| {
|
||||
AppError::ForbiddenError(
|
||||
"Mentor profile not found for current user".to_string(),
|
||||
)
|
||||
})?;
|
||||
Ok(ApiSuccess(MentorDetailResponseDto::from(detail)))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/mentors/me/status",
|
||||
responses(
|
||||
(status = 200, description = "[MENTOR] Mentor application status", body = String),
|
||||
(status = 401, description = "[MENTOR] Unauthorized - invalid token"),
|
||||
(status = 403, description = "[MENTOR] No mentor application found for current user"),
|
||||
(status = 500, description = "[MENTOR] Internal server error")
|
||||
),
|
||||
tag = "Mentors",
|
||||
security(("Bearer" = []))
|
||||
)]
|
||||
pub async fn get_mentor_status(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Extension(service): Extension<Arc<dyn MentorService>>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
require_permissions!(
|
||||
headers.clone(),
|
||||
state,
|
||||
[PermissionsEnum::ReadOwnMentorStatus],
|
||||
{
|
||||
let email = extract_email(&headers).ok_or_else(|| {
|
||||
AppError::AuthenticationError("Token tidak valid".to_string())
|
||||
})?;
|
||||
let status = service.get_status(&email).await.map_err(|_| {
|
||||
AppError::ForbiddenError(
|
||||
"No mentor application found for current user".to_string(),
|
||||
)
|
||||
})?;
|
||||
Ok(ApiMessage::ok(&status))
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,47 +1,56 @@
|
||||
use std::sync::Arc;
|
||||
use axum::{
|
||||
routing::{delete, get, post, put},
|
||||
Extension, Router,
|
||||
use super::handlers::{
|
||||
delete_mentor, get_mentor_by_id, get_mentor_list, get_mentor_me,
|
||||
get_mentor_status, post_register_mentor, put_update_mentor, put_update_mentor_me,
|
||||
put_update_mentor_no_id, put_verify_mentor,
|
||||
};
|
||||
use sea_orm::DatabaseConnection;
|
||||
use imphnen_libs::AppState;
|
||||
use imphnen_iam::users::infrastructure::persistence::PostgresUserRepository;
|
||||
use imphnen_iam::roles::infrastructure::persistence::PostgresRoleRepository;
|
||||
use crate::mentors::application::MentorServiceImpl;
|
||||
use crate::mentors::domain::MentorService;
|
||||
use crate::mentors::infrastructure::persistence::PostgresMentorRepository;
|
||||
use super::handlers::{
|
||||
delete_mentor, get_mentor_by_id, get_mentor_list, get_mentor_me, get_mentor_status,
|
||||
post_register_mentor, put_update_mentor, put_update_mentor_me, put_update_mentor_no_id,
|
||||
put_verify_mentor,
|
||||
use axum::{
|
||||
Extension, Router,
|
||||
routing::{delete, get, post, put},
|
||||
};
|
||||
use imphnen_iam::roles::infrastructure::persistence::PostgresRoleRepository;
|
||||
use imphnen_iam::users::infrastructure::persistence::PostgresUserRepository;
|
||||
use imphnen_libs::AppState;
|
||||
use sea_orm::DatabaseConnection;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn build_service(db: DatabaseConnection, state: Arc<AppState>) -> Arc<dyn MentorService> {
|
||||
let user_repo = Arc::new(PostgresUserRepository::new(db.clone()));
|
||||
let role_repo = Arc::new(PostgresRoleRepository::new(db.clone()));
|
||||
let repo = Arc::new(PostgresMentorRepository::new(db));
|
||||
Arc::new(MentorServiceImpl::new(repo, state, user_repo, role_repo))
|
||||
fn build_service(
|
||||
db: DatabaseConnection,
|
||||
state: Arc<AppState>,
|
||||
) -> Arc<dyn MentorService> {
|
||||
let user_repo = Arc::new(PostgresUserRepository::new(db.clone()));
|
||||
let role_repo = Arc::new(PostgresRoleRepository::new(db.clone()));
|
||||
let repo = Arc::new(PostgresMentorRepository::new(db));
|
||||
Arc::new(MentorServiceImpl::new(repo, state, user_repo, role_repo))
|
||||
}
|
||||
|
||||
pub fn mentors_public_routes(db: DatabaseConnection, state: Arc<AppState>) -> Router {
|
||||
let service = build_service(db, state);
|
||||
Router::new()
|
||||
.route("/mentors/create", post(post_register_mentor))
|
||||
.layer(Extension(service))
|
||||
pub fn mentors_public_routes(
|
||||
db: DatabaseConnection,
|
||||
state: Arc<AppState>,
|
||||
) -> Router {
|
||||
let service = build_service(db, state);
|
||||
Router::new()
|
||||
.route("/mentors/create", post(post_register_mentor))
|
||||
.layer(Extension(service))
|
||||
}
|
||||
|
||||
pub fn mentors_protected_routes(db: DatabaseConnection, state: Arc<AppState>) -> Router {
|
||||
let svc = build_service(db, Arc::clone(&state));
|
||||
Router::new()
|
||||
.route("/mentors", get(get_mentor_list))
|
||||
.route("/mentors/me", get(get_mentor_me))
|
||||
.route("/mentors/me/update", put(put_update_mentor_me))
|
||||
.route("/mentors/me/status", get(get_mentor_status))
|
||||
.route("/mentors/detail/{id}", get(get_mentor_by_id))
|
||||
.route("/mentors/update/{id}", put(put_update_mentor))
|
||||
.route("/mentors/update", put(put_update_mentor_no_id))
|
||||
.route("/mentors/delete/{id}", delete(delete_mentor))
|
||||
.route("/mentors/verify/{id}", put(put_verify_mentor))
|
||||
.layer(Extension(svc))
|
||||
.layer(Extension((*state).clone()))
|
||||
pub fn mentors_protected_routes(
|
||||
db: DatabaseConnection,
|
||||
state: Arc<AppState>,
|
||||
) -> Router {
|
||||
let svc = build_service(db, Arc::clone(&state));
|
||||
Router::new()
|
||||
.route("/mentors", get(get_mentor_list))
|
||||
.route("/mentors/me", get(get_mentor_me))
|
||||
.route("/mentors/me/update", put(put_update_mentor_me))
|
||||
.route("/mentors/me/status", get(get_mentor_status))
|
||||
.route("/mentors/detail/{id}", get(get_mentor_by_id))
|
||||
.route("/mentors/update/{id}", put(put_update_mentor))
|
||||
.route("/mentors/update", put(put_update_mentor_no_id))
|
||||
.route("/mentors/delete/{id}", delete(delete_mentor))
|
||||
.route("/mentors/verify/{id}", put(put_verify_mentor))
|
||||
.layer(Extension(svc))
|
||||
.layer(Extension((*state).clone()))
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod postgres_mentor_queries;
|
||||
pub mod postgres_mentor_repository;
|
||||
pub mod postgres_mentor_write;
|
||||
|
||||
pub use postgres_mentor_repository::PostgresMentorRepository;
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
use super::postgres_mentor_repository::model_to_entity;
|
||||
use crate::mentors::domain::mentor::MentorEntity;
|
||||
use imphnen_entities::seaorm::auth::mentors::{
|
||||
Column as MentorColumn, Entity as MentorsEntity,
|
||||
};
|
||||
use imphnen_utils::AppError;
|
||||
use paginator_rs::{PaginationParams, SortDirection};
|
||||
use paginator_utils::{PaginatorResponse, PaginatorResponseMeta};
|
||||
use sea_orm::prelude::*;
|
||||
use sea_orm::{Order, PaginatorTrait, QueryOrder};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub async fn find_all_paginated(
|
||||
db: &Arc<DatabaseConnection>,
|
||||
params: PaginationParams,
|
||||
) -> Result<PaginatorResponse<MentorEntity>, AppError> {
|
||||
let page = params.page.max(1);
|
||||
let per_page = params.per_page.clamp(1, 100);
|
||||
|
||||
let mut query = MentorsEntity::find().filter(MentorColumn::IsDeleted.eq(false));
|
||||
|
||||
query = match params.sort_by.as_deref() {
|
||||
Some("updated_at") => match params.sort_direction {
|
||||
Some(SortDirection::Asc) => {
|
||||
query.order_by(MentorColumn::UpdatedAt, Order::Asc)
|
||||
}
|
||||
_ => query.order_by(MentorColumn::UpdatedAt, Order::Desc),
|
||||
},
|
||||
_ => match params.sort_direction {
|
||||
Some(SortDirection::Asc) => {
|
||||
query.order_by(MentorColumn::CreatedAt, Order::Asc)
|
||||
}
|
||||
_ => query.order_by(MentorColumn::CreatedAt, Order::Desc),
|
||||
},
|
||||
};
|
||||
|
||||
let paginator = query.paginate(db.as_ref(), per_page as u64);
|
||||
let total = paginator
|
||||
.num_items()
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
let mentors = paginator
|
||||
.fetch_page((page - 1) as u64)
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
let data = mentors.into_iter().map(model_to_entity).collect();
|
||||
let meta = PaginatorResponseMeta::new(page, per_page, total as u32);
|
||||
Ok(PaginatorResponse { data, meta })
|
||||
}
|
||||
|
||||
pub async fn find_by_user_id(
|
||||
db: &Arc<DatabaseConnection>,
|
||||
user_id: Uuid,
|
||||
include_deleted: bool,
|
||||
) -> Result<MentorEntity, AppError> {
|
||||
let mut query = MentorsEntity::find().filter(MentorColumn::UserId.eq(user_id));
|
||||
|
||||
if !include_deleted {
|
||||
query = query.filter(MentorColumn::IsDeleted.eq(false));
|
||||
}
|
||||
|
||||
let model = query
|
||||
.one(db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||
.ok_or_else(|| AppError::NotFoundError("Mentor not found".to_string()))?;
|
||||
|
||||
Ok(model_to_entity(model))
|
||||
}
|
||||
+166
-265
@@ -1,283 +1,184 @@
|
||||
use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use sea_orm::prelude::*;
|
||||
use sea_orm::{ActiveValue, Order, QueryOrder, PaginatorTrait};
|
||||
use paginator_rs::{PaginationParams, SortDirection};
|
||||
use paginator_utils::{PaginatorResponse, PaginatorResponseMeta};
|
||||
use uuid::Uuid;
|
||||
use imphnen_utils::AppError;
|
||||
use imphnen_entities::seaorm::auth::mentors::{
|
||||
Entity as MentorsEntity,
|
||||
Column as MentorColumn,
|
||||
ActiveModel as MentorActiveModel,
|
||||
Model as MentorModel,
|
||||
};
|
||||
use super::postgres_mentor_queries;
|
||||
use super::postgres_mentor_write::apply_entity_to_model;
|
||||
use crate::mentors::domain::{mentor::MentorEntity, repository::MentorRepository};
|
||||
use async_trait::async_trait;
|
||||
use imphnen_entities::seaorm::auth::mentors::{
|
||||
ActiveModel as MentorActiveModel, Column as MentorColumn, Entity as MentorsEntity,
|
||||
Model as MentorModel,
|
||||
};
|
||||
use imphnen_utils::AppError;
|
||||
use paginator_rs::PaginationParams;
|
||||
use paginator_utils::PaginatorResponse;
|
||||
use sea_orm::ActiveValue;
|
||||
use sea_orm::prelude::*;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
fn model_to_entity(model: MentorModel) -> MentorEntity {
|
||||
MentorEntity {
|
||||
id: model.id,
|
||||
user_id: model.user_id,
|
||||
industries: serde_json::from_value(
|
||||
model.industries.unwrap_or(serde_json::Value::Array(vec![])),
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
expertise: serde_json::from_value(
|
||||
model.expertise.unwrap_or(serde_json::Value::Array(vec![])),
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
languages: serde_json::from_value(
|
||||
model.languages.unwrap_or(serde_json::Value::Array(vec![])),
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
current_company: model.current_company.unwrap_or_default(),
|
||||
current_role: model.current_role.unwrap_or_default(),
|
||||
years_of_experience: model.years_of_experience.unwrap_or(0),
|
||||
topics_of_interest: serde_json::from_value(
|
||||
model.topics_of_interest.unwrap_or(serde_json::Value::Array(vec![])),
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
preferred_mentee_level: serde_json::from_str(
|
||||
&model.preferred_mentee_level.unwrap_or_default(),
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
preferred_mentoring_formats: serde_json::from_value(
|
||||
model
|
||||
.preferred_mentoring_formats
|
||||
.unwrap_or(serde_json::Value::Array(vec![])),
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
availability_commitment: model.availability_commitment.unwrap_or_default(),
|
||||
mentoring_rate: model.mentoring_rate.unwrap_or(0.0),
|
||||
status: model.status.unwrap_or_default(),
|
||||
is_deleted: model.is_deleted,
|
||||
created_at: model.created_at,
|
||||
updated_at: model.updated_at,
|
||||
}
|
||||
pub fn model_to_entity(model: MentorModel) -> MentorEntity {
|
||||
MentorEntity {
|
||||
id: model.id,
|
||||
user_id: model.user_id,
|
||||
industries: serde_json::from_value(
|
||||
model.industries.unwrap_or(serde_json::Value::Array(vec![])),
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
expertise: serde_json::from_value(
|
||||
model.expertise.unwrap_or(serde_json::Value::Array(vec![])),
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
languages: serde_json::from_value(
|
||||
model.languages.unwrap_or(serde_json::Value::Array(vec![])),
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
current_company: model.current_company.unwrap_or_default(),
|
||||
current_role: model.current_role.unwrap_or_default(),
|
||||
years_of_experience: model.years_of_experience.unwrap_or(0),
|
||||
topics_of_interest: serde_json::from_value(
|
||||
model
|
||||
.topics_of_interest
|
||||
.unwrap_or(serde_json::Value::Array(vec![])),
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
preferred_mentee_level: serde_json::from_str(
|
||||
&model.preferred_mentee_level.unwrap_or_default(),
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
preferred_mentoring_formats: serde_json::from_value(
|
||||
model
|
||||
.preferred_mentoring_formats
|
||||
.unwrap_or(serde_json::Value::Array(vec![])),
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
availability_commitment: model.availability_commitment.unwrap_or_default(),
|
||||
mentoring_rate: model.mentoring_rate.unwrap_or(0.0),
|
||||
status: model.status.unwrap_or_default(),
|
||||
is_deleted: model.is_deleted,
|
||||
created_at: model.created_at,
|
||||
updated_at: model.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PostgresMentorRepository {
|
||||
db: Arc<DatabaseConnection>,
|
||||
pub db: Arc<DatabaseConnection>,
|
||||
}
|
||||
|
||||
impl PostgresMentorRepository {
|
||||
pub fn new(db: DatabaseConnection) -> Self {
|
||||
Self { db: Arc::new(db) }
|
||||
}
|
||||
pub fn new(db: DatabaseConnection) -> Self {
|
||||
Self { db: Arc::new(db) }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl MentorRepository for PostgresMentorRepository {
|
||||
async fn find_all(
|
||||
&self,
|
||||
params: PaginationParams,
|
||||
) -> Result<PaginatorResponse<MentorEntity>, AppError> {
|
||||
let page = params.page.max(1);
|
||||
let per_page = params.per_page.clamp(1, 100);
|
||||
async fn find_all(
|
||||
&self,
|
||||
params: PaginationParams,
|
||||
) -> Result<PaginatorResponse<MentorEntity>, AppError> {
|
||||
postgres_mentor_queries::find_all_paginated(&self.db, params).await
|
||||
}
|
||||
|
||||
let mut query = MentorsEntity::find()
|
||||
.filter(MentorColumn::IsDeleted.eq(false));
|
||||
async fn find_by_id(
|
||||
&self,
|
||||
id: Uuid,
|
||||
include_deleted: bool,
|
||||
) -> Result<MentorEntity, AppError> {
|
||||
let mut query = MentorsEntity::find_by_id(id);
|
||||
if !include_deleted {
|
||||
query = query.filter(MentorColumn::IsDeleted.eq(false));
|
||||
}
|
||||
let model = query
|
||||
.one(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||
.ok_or_else(|| AppError::NotFoundError("Mentor not found".to_string()))?;
|
||||
Ok(model_to_entity(model))
|
||||
}
|
||||
|
||||
query = match params.sort_by.as_deref() {
|
||||
Some("updated_at") => match params.sort_direction {
|
||||
Some(SortDirection::Asc) => query.order_by(MentorColumn::UpdatedAt, Order::Asc),
|
||||
_ => query.order_by(MentorColumn::UpdatedAt, Order::Desc),
|
||||
},
|
||||
_ => match params.sort_direction {
|
||||
Some(SortDirection::Asc) => query.order_by(MentorColumn::CreatedAt, Order::Asc),
|
||||
_ => query.order_by(MentorColumn::CreatedAt, Order::Desc),
|
||||
},
|
||||
};
|
||||
async fn find_by_user_id(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
include_deleted: bool,
|
||||
) -> Result<MentorEntity, AppError> {
|
||||
postgres_mentor_queries::find_by_user_id(&self.db, user_id, include_deleted)
|
||||
.await
|
||||
}
|
||||
|
||||
let paginator = query.paginate(self.db.as_ref(), per_page as u64);
|
||||
let total = paginator
|
||||
.num_items()
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
let mentors = paginator
|
||||
.fetch_page((page - 1) as u64)
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
async fn create(&self, entity: MentorEntity) -> Result<Uuid, AppError> {
|
||||
let active_model = MentorActiveModel {
|
||||
user_id: ActiveValue::Set(entity.user_id),
|
||||
industries: ActiveValue::Set(Some(
|
||||
serde_json::to_value(&entity.industries)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||
)),
|
||||
expertise: ActiveValue::Set(Some(
|
||||
serde_json::to_value(&entity.expertise)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||
)),
|
||||
languages: ActiveValue::Set(Some(
|
||||
serde_json::to_value(&entity.languages)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||
)),
|
||||
current_company: ActiveValue::Set(Some(entity.current_company)),
|
||||
current_role: ActiveValue::Set(Some(entity.current_role)),
|
||||
years_of_experience: ActiveValue::Set(Some(entity.years_of_experience)),
|
||||
topics_of_interest: ActiveValue::Set(Some(
|
||||
serde_json::to_value(&entity.topics_of_interest)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||
)),
|
||||
preferred_mentee_level: ActiveValue::Set(Some(
|
||||
serde_json::to_string(&entity.preferred_mentee_level)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||
)),
|
||||
preferred_mentoring_formats: ActiveValue::Set(Some(
|
||||
serde_json::to_value(&entity.preferred_mentoring_formats)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||
)),
|
||||
availability_commitment: ActiveValue::Set(Some(
|
||||
entity.availability_commitment,
|
||||
)),
|
||||
mentoring_rate: ActiveValue::Set(Some(entity.mentoring_rate)),
|
||||
status: ActiveValue::Set(Some(entity.status)),
|
||||
is_deleted: ActiveValue::Set(false),
|
||||
created_at: ActiveValue::Set(chrono::Utc::now()),
|
||||
updated_at: ActiveValue::Set(chrono::Utc::now()),
|
||||
..Default::default()
|
||||
};
|
||||
let result = MentorsEntity::insert(active_model)
|
||||
.exec(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(result.last_insert_id)
|
||||
}
|
||||
|
||||
let data = mentors.into_iter().map(model_to_entity).collect();
|
||||
let meta = PaginatorResponseMeta::new(page, per_page, total as u32);
|
||||
Ok(PaginatorResponse { data, meta })
|
||||
}
|
||||
async fn update(&self, entity: MentorEntity) -> Result<(), AppError> {
|
||||
let mut active_model: MentorActiveModel = MentorsEntity::find_by_id(entity.id)
|
||||
.one(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||
.ok_or_else(|| AppError::NotFoundError("Mentor not found".to_string()))?
|
||||
.into();
|
||||
apply_entity_to_model(&entity, &mut active_model)?;
|
||||
active_model
|
||||
.update(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn find_by_id(
|
||||
&self,
|
||||
id: Uuid,
|
||||
include_deleted: bool,
|
||||
) -> Result<MentorEntity, AppError> {
|
||||
let mut query = MentorsEntity::find_by_id(id);
|
||||
|
||||
if !include_deleted {
|
||||
query = query.filter(MentorColumn::IsDeleted.eq(false));
|
||||
}
|
||||
|
||||
let model = query
|
||||
.one(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||
.ok_or_else(|| AppError::NotFoundError("Mentor not found".to_string()))?;
|
||||
|
||||
Ok(model_to_entity(model))
|
||||
}
|
||||
|
||||
async fn find_by_user_id(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
include_deleted: bool,
|
||||
) -> Result<MentorEntity, AppError> {
|
||||
let mut query = MentorsEntity::find()
|
||||
.filter(MentorColumn::UserId.eq(user_id));
|
||||
|
||||
if !include_deleted {
|
||||
query = query.filter(MentorColumn::IsDeleted.eq(false));
|
||||
}
|
||||
|
||||
let model = query
|
||||
.one(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||
.ok_or_else(|| AppError::NotFoundError("Mentor not found".to_string()))?;
|
||||
|
||||
Ok(model_to_entity(model))
|
||||
}
|
||||
|
||||
async fn create(&self, entity: MentorEntity) -> Result<Uuid, AppError> {
|
||||
let active_model = MentorActiveModel {
|
||||
user_id: ActiveValue::Set(entity.user_id),
|
||||
industries: ActiveValue::Set(Some(
|
||||
serde_json::to_value(&entity.industries)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||
)),
|
||||
expertise: ActiveValue::Set(Some(
|
||||
serde_json::to_value(&entity.expertise)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||
)),
|
||||
languages: ActiveValue::Set(Some(
|
||||
serde_json::to_value(&entity.languages)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||
)),
|
||||
current_company: ActiveValue::Set(Some(entity.current_company)),
|
||||
current_role: ActiveValue::Set(Some(entity.current_role)),
|
||||
years_of_experience: ActiveValue::Set(Some(entity.years_of_experience)),
|
||||
topics_of_interest: ActiveValue::Set(Some(
|
||||
serde_json::to_value(&entity.topics_of_interest)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||
)),
|
||||
preferred_mentee_level: ActiveValue::Set(Some(
|
||||
serde_json::to_string(&entity.preferred_mentee_level)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||
)),
|
||||
preferred_mentoring_formats: ActiveValue::Set(Some(
|
||||
serde_json::to_value(&entity.preferred_mentoring_formats)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||
)),
|
||||
availability_commitment: ActiveValue::Set(Some(entity.availability_commitment)),
|
||||
mentoring_rate: ActiveValue::Set(Some(entity.mentoring_rate)),
|
||||
status: ActiveValue::Set(Some(entity.status)),
|
||||
is_deleted: ActiveValue::Set(false),
|
||||
created_at: ActiveValue::Set(chrono::Utc::now()),
|
||||
updated_at: ActiveValue::Set(chrono::Utc::now()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = MentorsEntity::insert(active_model)
|
||||
.exec(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
Ok(result.last_insert_id)
|
||||
}
|
||||
|
||||
async fn update(&self, entity: MentorEntity) -> Result<(), AppError> {
|
||||
let mut active_model: MentorActiveModel = MentorsEntity::find_by_id(entity.id)
|
||||
.one(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||
.ok_or_else(|| AppError::NotFoundError("Mentor not found".to_string()))?
|
||||
.into();
|
||||
|
||||
if !entity.industries.is_empty() {
|
||||
active_model.industries = ActiveValue::Set(Some(
|
||||
serde_json::to_value(&entity.industries)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||
));
|
||||
}
|
||||
if !entity.expertise.is_empty() {
|
||||
active_model.expertise = ActiveValue::Set(Some(
|
||||
serde_json::to_value(&entity.expertise)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||
));
|
||||
}
|
||||
if !entity.languages.is_empty() {
|
||||
active_model.languages = ActiveValue::Set(Some(
|
||||
serde_json::to_value(&entity.languages)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||
));
|
||||
}
|
||||
if !entity.current_company.is_empty() {
|
||||
active_model.current_company = ActiveValue::Set(Some(entity.current_company));
|
||||
}
|
||||
if !entity.current_role.is_empty() {
|
||||
active_model.current_role = ActiveValue::Set(Some(entity.current_role));
|
||||
}
|
||||
active_model.years_of_experience = ActiveValue::Set(Some(entity.years_of_experience));
|
||||
if !entity.topics_of_interest.is_empty() {
|
||||
active_model.topics_of_interest = ActiveValue::Set(Some(
|
||||
serde_json::to_value(&entity.topics_of_interest)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||
));
|
||||
}
|
||||
if !entity.preferred_mentee_level.is_empty() {
|
||||
active_model.preferred_mentee_level = ActiveValue::Set(Some(
|
||||
serde_json::to_string(&entity.preferred_mentee_level)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||
));
|
||||
}
|
||||
if !entity.preferred_mentoring_formats.is_empty() {
|
||||
active_model.preferred_mentoring_formats = ActiveValue::Set(Some(
|
||||
serde_json::to_value(&entity.preferred_mentoring_formats)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||
));
|
||||
}
|
||||
if !entity.availability_commitment.is_empty() {
|
||||
active_model.availability_commitment =
|
||||
ActiveValue::Set(Some(entity.availability_commitment));
|
||||
}
|
||||
active_model.mentoring_rate = ActiveValue::Set(Some(entity.mentoring_rate));
|
||||
if !entity.status.is_empty() {
|
||||
active_model.status = ActiveValue::Set(Some(entity.status));
|
||||
}
|
||||
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
||||
|
||||
active_model
|
||||
.update(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn soft_delete(&self, id: Uuid) -> Result<(), AppError> {
|
||||
let model = MentorsEntity::find_by_id(id)
|
||||
.filter(MentorColumn::IsDeleted.eq(false))
|
||||
.one(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||
.ok_or_else(|| AppError::NotFoundError("Mentor not found".to_string()))?;
|
||||
|
||||
let mut active_model: MentorActiveModel = model.into();
|
||||
active_model.is_deleted = ActiveValue::Set(true);
|
||||
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
||||
|
||||
active_model
|
||||
.update(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
async fn soft_delete(&self, id: Uuid) -> Result<(), AppError> {
|
||||
let model = MentorsEntity::find_by_id(id)
|
||||
.filter(MentorColumn::IsDeleted.eq(false))
|
||||
.one(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||
.ok_or_else(|| AppError::NotFoundError("Mentor not found".to_string()))?;
|
||||
let mut active_model: MentorActiveModel = model.into();
|
||||
active_model.is_deleted = ActiveValue::Set(true);
|
||||
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
||||
active_model
|
||||
.update(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
use crate::mentors::domain::mentor::MentorEntity;
|
||||
use imphnen_entities::seaorm::auth::mentors::ActiveModel as MentorActiveModel;
|
||||
use imphnen_utils::AppError;
|
||||
use sea_orm::ActiveValue;
|
||||
|
||||
pub fn apply_entity_to_model(
|
||||
entity: &MentorEntity,
|
||||
active_model: &mut MentorActiveModel,
|
||||
) -> Result<(), AppError> {
|
||||
if !entity.industries.is_empty() {
|
||||
active_model.industries = ActiveValue::Set(Some(
|
||||
serde_json::to_value(&entity.industries)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||
));
|
||||
}
|
||||
if !entity.expertise.is_empty() {
|
||||
active_model.expertise = ActiveValue::Set(Some(
|
||||
serde_json::to_value(&entity.expertise)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||
));
|
||||
}
|
||||
if !entity.languages.is_empty() {
|
||||
active_model.languages = ActiveValue::Set(Some(
|
||||
serde_json::to_value(&entity.languages)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||
));
|
||||
}
|
||||
if !entity.current_company.is_empty() {
|
||||
active_model.current_company =
|
||||
ActiveValue::Set(Some(entity.current_company.clone()));
|
||||
}
|
||||
if !entity.current_role.is_empty() {
|
||||
active_model.current_role = ActiveValue::Set(Some(entity.current_role.clone()));
|
||||
}
|
||||
active_model.years_of_experience =
|
||||
ActiveValue::Set(Some(entity.years_of_experience));
|
||||
if !entity.topics_of_interest.is_empty() {
|
||||
active_model.topics_of_interest = ActiveValue::Set(Some(
|
||||
serde_json::to_value(&entity.topics_of_interest)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||
));
|
||||
}
|
||||
if !entity.preferred_mentee_level.is_empty() {
|
||||
active_model.preferred_mentee_level = ActiveValue::Set(Some(
|
||||
serde_json::to_string(&entity.preferred_mentee_level)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||
));
|
||||
}
|
||||
if !entity.preferred_mentoring_formats.is_empty() {
|
||||
active_model.preferred_mentoring_formats = ActiveValue::Set(Some(
|
||||
serde_json::to_value(&entity.preferred_mentoring_formats)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?,
|
||||
));
|
||||
}
|
||||
if !entity.availability_commitment.is_empty() {
|
||||
active_model.availability_commitment =
|
||||
ActiveValue::Set(Some(entity.availability_commitment.clone()));
|
||||
}
|
||||
active_model.mentoring_rate = ActiveValue::Set(Some(entity.mentoring_rate));
|
||||
if !entity.status.is_empty() {
|
||||
active_model.status = ActiveValue::Set(Some(entity.status.clone()));
|
||||
}
|
||||
active_model.updated_at = ActiveValue::Set(chrono::Utc::now());
|
||||
Ok(())
|
||||
}
|
||||
@@ -2,4 +2,6 @@ pub mod application;
|
||||
pub mod domain;
|
||||
pub mod infrastructure;
|
||||
|
||||
pub use infrastructure::http::routes::{mentors_protected_routes, mentors_public_routes};
|
||||
pub use infrastructure::http::routes::{
|
||||
mentors_protected_routes, mentors_public_routes,
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod session_booking_service;
|
||||
pub mod session_query_service;
|
||||
pub mod session_service;
|
||||
|
||||
pub use session_service::SessionServiceImpl;
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
use crate::sessions::domain::{
|
||||
BookSessionCommand, BookedSession, SessionEntity, SessionFeedbackCommand,
|
||||
SessionFeedbackResult, SessionRepository, UpdateSessionStatusCommand,
|
||||
UpdatedSessionStatus,
|
||||
};
|
||||
use chrono::{DateTime, Utc};
|
||||
use imphnen_utils::AppError;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct SessionBookingService {
|
||||
pub repo: Arc<dyn SessionRepository>,
|
||||
}
|
||||
|
||||
impl SessionBookingService {
|
||||
pub async fn book_session(
|
||||
&self,
|
||||
mentor_id: String,
|
||||
user_id: String,
|
||||
cmd: BookSessionCommand,
|
||||
) -> Result<BookedSession, AppError> {
|
||||
let scheduled_at = DateTime::parse_from_rfc3339(&cmd.scheduled_at)
|
||||
.map_err(|e| {
|
||||
AppError::BadRequestError(format!("Invalid scheduled_at format: {}", e))
|
||||
})?
|
||||
.with_timezone(&Utc);
|
||||
|
||||
let mentor_uuid = Uuid::parse_str(&mentor_id)
|
||||
.map_err(|e| AppError::BadRequestError(format!("Invalid mentor ID: {}", e)))?;
|
||||
|
||||
let mentee_uuid = Uuid::parse_str(&user_id)
|
||||
.map_err(|e| AppError::BadRequestError(format!("Invalid user ID: {}", e)))?;
|
||||
|
||||
let entity = SessionEntity {
|
||||
id: Uuid::new_v4(),
|
||||
mentor_id: mentor_uuid,
|
||||
mentee_id: mentee_uuid,
|
||||
topic: cmd.topic,
|
||||
description: cmd.description,
|
||||
scheduled_at,
|
||||
duration_minutes: cmd.duration_minutes.unwrap_or(60),
|
||||
meeting_link: None,
|
||||
session_type: cmd.session_type.unwrap_or_else(|| "video_call".to_string()),
|
||||
status: "pending".to_string(),
|
||||
feedback: None,
|
||||
rating: None,
|
||||
feedback_submitted_at: None,
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
};
|
||||
|
||||
let created = self.repo.create(entity).await?;
|
||||
|
||||
Ok(BookedSession {
|
||||
id: created.id.to_string(),
|
||||
mentor_id: created.mentor_id.to_string(),
|
||||
mentee_id: created.mentee_id.to_string(),
|
||||
topic: created.topic,
|
||||
description: created.description,
|
||||
scheduled_at: created.scheduled_at.to_rfc3339(),
|
||||
duration_minutes: created.duration_minutes,
|
||||
session_type: created.session_type,
|
||||
status: created.status,
|
||||
created_at: created.created_at.to_rfc3339(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn update_session_status(
|
||||
&self,
|
||||
session_id: String,
|
||||
_user_id: String,
|
||||
cmd: UpdateSessionStatusCommand,
|
||||
) -> Result<UpdatedSessionStatus, AppError> {
|
||||
let session_uuid = Uuid::parse_str(&session_id).map_err(|e| {
|
||||
AppError::BadRequestError(format!("Invalid session ID: {}", e))
|
||||
})?;
|
||||
|
||||
let mut session = self
|
||||
.repo
|
||||
.find_by_id(session_uuid)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::NotFoundError("Session not found".to_string()))?;
|
||||
|
||||
session.status = cmd.status;
|
||||
if let Some(link) = cmd.meeting_link {
|
||||
session.meeting_link = Some(link);
|
||||
}
|
||||
session.updated_at = Utc::now();
|
||||
|
||||
let updated = self.repo.update(session_uuid, session).await?;
|
||||
|
||||
Ok(UpdatedSessionStatus {
|
||||
id: updated.id.to_string(),
|
||||
status: updated.status,
|
||||
meeting_link: updated.meeting_link,
|
||||
updated_at: updated.updated_at.to_rfc3339(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn submit_feedback(
|
||||
&self,
|
||||
session_id: String,
|
||||
user_id: String,
|
||||
cmd: SessionFeedbackCommand,
|
||||
) -> Result<SessionFeedbackResult, AppError> {
|
||||
let session_uuid = Uuid::parse_str(&session_id).map_err(|e| {
|
||||
AppError::BadRequestError(format!("Invalid session ID: {}", e))
|
||||
})?;
|
||||
|
||||
let mut session = self
|
||||
.repo
|
||||
.find_by_id(session_uuid)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::NotFoundError("Session not found".to_string()))?;
|
||||
|
||||
if session.mentee_id.to_string() != user_id {
|
||||
return Err(AppError::ForbiddenError(
|
||||
"Only the mentee can submit feedback".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if session.status != "completed" {
|
||||
return Err(AppError::BadRequestError(
|
||||
"Feedback can only be submitted for completed sessions".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
session.feedback = Some(cmd.feedback.clone());
|
||||
session.rating = Some(cmd.rating);
|
||||
session.feedback_submitted_at = Some(Utc::now());
|
||||
session.updated_at = Utc::now();
|
||||
|
||||
let updated = self.repo.update(session_uuid, session).await?;
|
||||
let submitted_at = updated
|
||||
.feedback_submitted_at
|
||||
.unwrap_or_else(Utc::now)
|
||||
.to_rfc3339();
|
||||
|
||||
Ok(SessionFeedbackResult {
|
||||
id: updated.id.to_string(),
|
||||
feedback: cmd.feedback,
|
||||
rating: cmd.rating,
|
||||
submitted_at,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
use crate::sessions::domain::{
|
||||
AvailabilitySlot, MentorAvailability, SessionDetail, SessionList, SessionListItem,
|
||||
SessionRepository,
|
||||
};
|
||||
use chrono::{Duration, Utc};
|
||||
use imphnen_utils::AppError;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct SessionQueryService {
|
||||
pub repo: Arc<dyn SessionRepository>,
|
||||
}
|
||||
|
||||
impl SessionQueryService {
|
||||
pub async fn get_mentor_sessions(
|
||||
&self,
|
||||
mentor_id: String,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<SessionList, AppError> {
|
||||
let mentor_uuid = Uuid::parse_str(&mentor_id)
|
||||
.map_err(|e| AppError::BadRequestError(format!("Invalid mentor ID: {}", e)))?;
|
||||
|
||||
let count = self
|
||||
.repo
|
||||
.count_by_mentor(mentor_uuid, status_filter.clone())
|
||||
.await?;
|
||||
|
||||
let sessions = self
|
||||
.repo
|
||||
.find_by_mentor_id(mentor_uuid, status_filter)
|
||||
.await?;
|
||||
|
||||
let items: Vec<SessionListItem> = sessions
|
||||
.into_iter()
|
||||
.map(|s| SessionListItem {
|
||||
id: s.id.to_string(),
|
||||
mentor_id: s.mentor_id.to_string(),
|
||||
mentee_id: s.mentee_id.to_string(),
|
||||
mentee_fullname: None,
|
||||
mentee_email: None,
|
||||
topic: s.topic,
|
||||
scheduled_at: s.scheduled_at.to_rfc3339(),
|
||||
duration_minutes: s.duration_minutes,
|
||||
session_type: s.session_type,
|
||||
status: s.status,
|
||||
rating: s.rating,
|
||||
created_at: s.created_at.to_rfc3339(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(SessionList {
|
||||
sessions: items,
|
||||
total: count,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_user_sessions(
|
||||
&self,
|
||||
user_id: String,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<SessionList, AppError> {
|
||||
let user_uuid = Uuid::parse_str(&user_id)
|
||||
.map_err(|e| AppError::BadRequestError(format!("Invalid user ID: {}", e)))?;
|
||||
|
||||
let count = self
|
||||
.repo
|
||||
.count_by_mentee(user_uuid, status_filter.clone())
|
||||
.await?;
|
||||
|
||||
let sessions = self
|
||||
.repo
|
||||
.find_by_mentee_id(user_uuid, status_filter)
|
||||
.await?;
|
||||
|
||||
let items: Vec<SessionListItem> = sessions
|
||||
.into_iter()
|
||||
.map(|s| SessionListItem {
|
||||
id: s.id.to_string(),
|
||||
mentor_id: s.mentor_id.to_string(),
|
||||
mentee_id: s.mentee_id.to_string(),
|
||||
mentee_fullname: None,
|
||||
mentee_email: None,
|
||||
topic: s.topic,
|
||||
scheduled_at: s.scheduled_at.to_rfc3339(),
|
||||
duration_minutes: s.duration_minutes,
|
||||
session_type: s.session_type,
|
||||
status: s.status,
|
||||
rating: s.rating,
|
||||
created_at: s.created_at.to_rfc3339(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(SessionList {
|
||||
sessions: items,
|
||||
total: count,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_mentor_availability(
|
||||
&self,
|
||||
mentor_id: String,
|
||||
) -> Result<MentorAvailability, AppError> {
|
||||
let mentor_uuid = Uuid::parse_str(&mentor_id)
|
||||
.map_err(|e| AppError::BadRequestError(format!("Invalid mentor ID: {}", e)))?;
|
||||
|
||||
let booked_dates = self.repo.find_booked_dates(mentor_uuid).await?;
|
||||
|
||||
let mut slots = Vec::new();
|
||||
let today = Utc::now().date_naive();
|
||||
|
||||
for i in 0..7 {
|
||||
let date = today + Duration::days(i);
|
||||
let date_str = date.format("%Y-%m-%d").to_string();
|
||||
|
||||
for hour in 9..17 {
|
||||
let time_str = format!("{:02}:00", hour);
|
||||
let datetime_prefix = format!("{}T{}", date_str, time_str);
|
||||
|
||||
let is_booked = booked_dates
|
||||
.iter()
|
||||
.any(|d| d.starts_with(&datetime_prefix[..13]));
|
||||
|
||||
slots.push(AvailabilitySlot {
|
||||
date: date_str.clone(),
|
||||
time: time_str,
|
||||
available: !is_booked,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(MentorAvailability {
|
||||
mentor_id,
|
||||
availability_commitment: "Available weekdays 9 AM - 5 PM".to_string(),
|
||||
preferred_formats: vec!["video_call".to_string(), "phone_call".to_string()],
|
||||
slots,
|
||||
booked_dates,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_session_detail(
|
||||
&self,
|
||||
session_id: String,
|
||||
) -> Result<SessionDetail, AppError> {
|
||||
let session_uuid = Uuid::parse_str(&session_id).map_err(|e| {
|
||||
AppError::BadRequestError(format!("Invalid session ID: {}", e))
|
||||
})?;
|
||||
|
||||
let session = self
|
||||
.repo
|
||||
.find_by_id(session_uuid)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::NotFoundError("Session not found".to_string()))?;
|
||||
|
||||
Ok(SessionDetail {
|
||||
id: session.id.to_string(),
|
||||
mentor_id: session.mentor_id.to_string(),
|
||||
mentor_fullname: None,
|
||||
mentee_id: session.mentee_id.to_string(),
|
||||
mentee_fullname: None,
|
||||
topic: session.topic,
|
||||
description: session.description,
|
||||
scheduled_at: session.scheduled_at.to_rfc3339(),
|
||||
duration_minutes: session.duration_minutes,
|
||||
meeting_link: session.meeting_link,
|
||||
session_type: session.session_type,
|
||||
status: session.status,
|
||||
feedback: session.feedback,
|
||||
rating: session.rating,
|
||||
feedback_submitted_at: session.feedback_submitted_at.map(|dt| dt.to_rfc3339()),
|
||||
created_at: session.created_at.to_rfc3339(),
|
||||
updated_at: session.updated_at.to_rfc3339(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,310 +1,92 @@
|
||||
use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use uuid::Uuid;
|
||||
use imphnen_utils::AppError;
|
||||
use crate::sessions::domain::{SessionEntity, SessionRepository, SessionService};
|
||||
use crate::sessions::infrastructure::http::dto::{
|
||||
AvailabilitySlotDto, BookSessionRequestDto, BookSessionResponseDto, MentorAvailabilityDto,
|
||||
SessionDetailDto, SessionFeedbackRequestDto, SessionFeedbackResponseDto, SessionListItemDto,
|
||||
SessionListResponseDto, UpdateSessionStatusRequestDto, UpdateSessionStatusResponseDto,
|
||||
use super::session_booking_service::SessionBookingService;
|
||||
use super::session_query_service::SessionQueryService;
|
||||
use crate::sessions::domain::{
|
||||
BookSessionCommand, BookedSession, MentorAvailability, SessionDetail,
|
||||
SessionFeedbackCommand, SessionFeedbackResult, SessionList, SessionRepository,
|
||||
SessionService, UpdateSessionStatusCommand, UpdatedSessionStatus,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use imphnen_utils::AppError;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct SessionServiceImpl {
|
||||
repo: Arc<dyn SessionRepository>,
|
||||
booking: SessionBookingService,
|
||||
query: SessionQueryService,
|
||||
}
|
||||
|
||||
impl SessionServiceImpl {
|
||||
pub fn new(repo: Arc<dyn SessionRepository>) -> Self {
|
||||
Self { repo }
|
||||
}
|
||||
pub fn new(repo: Arc<dyn SessionRepository>) -> Self {
|
||||
Self {
|
||||
booking: SessionBookingService {
|
||||
repo: Arc::clone(&repo),
|
||||
},
|
||||
query: SessionQueryService { repo },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SessionService for SessionServiceImpl {
|
||||
async fn book_session(
|
||||
&self,
|
||||
mentor_id: String,
|
||||
user_id: String,
|
||||
dto: BookSessionRequestDto,
|
||||
) -> Result<BookSessionResponseDto, AppError> {
|
||||
let scheduled_at = DateTime::parse_from_rfc3339(&dto.scheduled_at)
|
||||
.map_err(|e| AppError::BadRequestError(format!("Invalid scheduled_at format: {}", e)))?
|
||||
.with_timezone(&Utc);
|
||||
async fn book_session(
|
||||
&self,
|
||||
mentor_id: String,
|
||||
user_id: String,
|
||||
cmd: BookSessionCommand,
|
||||
) -> Result<BookedSession, AppError> {
|
||||
self.booking.book_session(mentor_id, user_id, cmd).await
|
||||
}
|
||||
|
||||
let mentor_uuid = Uuid::parse_str(&mentor_id)
|
||||
.map_err(|e| AppError::BadRequestError(format!("Invalid mentor ID: {}", e)))?;
|
||||
async fn get_mentor_sessions(
|
||||
&self,
|
||||
mentor_id: String,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<SessionList, AppError> {
|
||||
self
|
||||
.query
|
||||
.get_mentor_sessions(mentor_id, status_filter)
|
||||
.await
|
||||
}
|
||||
|
||||
let mentee_uuid = Uuid::parse_str(&user_id)
|
||||
.map_err(|e| AppError::BadRequestError(format!("Invalid user ID: {}", e)))?;
|
||||
async fn get_user_sessions(
|
||||
&self,
|
||||
user_id: String,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<SessionList, AppError> {
|
||||
self.query.get_user_sessions(user_id, status_filter).await
|
||||
}
|
||||
|
||||
let entity = SessionEntity {
|
||||
id: Uuid::new_v4(),
|
||||
mentor_id: mentor_uuid,
|
||||
mentee_id: mentee_uuid,
|
||||
topic: dto.topic,
|
||||
description: dto.description,
|
||||
scheduled_at,
|
||||
duration_minutes: dto.duration_minutes.unwrap_or(60),
|
||||
meeting_link: None,
|
||||
session_type: dto.session_type.unwrap_or_else(|| "video_call".to_string()),
|
||||
status: "pending".to_string(),
|
||||
feedback: None,
|
||||
rating: None,
|
||||
feedback_submitted_at: None,
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
};
|
||||
async fn get_mentor_availability(
|
||||
&self,
|
||||
mentor_id: String,
|
||||
) -> Result<MentorAvailability, AppError> {
|
||||
self.query.get_mentor_availability(mentor_id).await
|
||||
}
|
||||
|
||||
let created = self.repo.create(entity).await?;
|
||||
async fn update_session_status(
|
||||
&self,
|
||||
session_id: String,
|
||||
user_id: String,
|
||||
cmd: UpdateSessionStatusCommand,
|
||||
) -> Result<UpdatedSessionStatus, AppError> {
|
||||
self
|
||||
.booking
|
||||
.update_session_status(session_id, user_id, cmd)
|
||||
.await
|
||||
}
|
||||
|
||||
Ok(BookSessionResponseDto {
|
||||
id: created.id.to_string(),
|
||||
mentor_id: created.mentor_id.to_string(),
|
||||
mentee_id: created.mentee_id.to_string(),
|
||||
topic: created.topic,
|
||||
description: created.description,
|
||||
scheduled_at: created.scheduled_at.to_rfc3339(),
|
||||
duration_minutes: created.duration_minutes,
|
||||
session_type: created.session_type,
|
||||
status: created.status,
|
||||
created_at: created.created_at.to_rfc3339(),
|
||||
})
|
||||
}
|
||||
async fn submit_feedback(
|
||||
&self,
|
||||
session_id: String,
|
||||
user_id: String,
|
||||
cmd: SessionFeedbackCommand,
|
||||
) -> Result<SessionFeedbackResult, AppError> {
|
||||
self.booking.submit_feedback(session_id, user_id, cmd).await
|
||||
}
|
||||
|
||||
async fn get_mentor_sessions(
|
||||
&self,
|
||||
mentor_id: String,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<SessionListResponseDto, AppError> {
|
||||
let mentor_uuid = Uuid::parse_str(&mentor_id)
|
||||
.map_err(|e| AppError::BadRequestError(format!("Invalid mentor ID: {}", e)))?;
|
||||
|
||||
let count = self
|
||||
.repo
|
||||
.count_by_mentor(mentor_uuid, status_filter.clone())
|
||||
.await?;
|
||||
|
||||
let sessions = self
|
||||
.repo
|
||||
.find_by_mentor_id(mentor_uuid, status_filter)
|
||||
.await?;
|
||||
|
||||
let items: Vec<SessionListItemDto> = sessions
|
||||
.into_iter()
|
||||
.map(|s| SessionListItemDto {
|
||||
id: s.id.to_string(),
|
||||
mentor_id: s.mentor_id.to_string(),
|
||||
mentee_id: s.mentee_id.to_string(),
|
||||
mentee_fullname: None,
|
||||
mentee_email: None,
|
||||
topic: s.topic,
|
||||
scheduled_at: s.scheduled_at.to_rfc3339(),
|
||||
duration_minutes: s.duration_minutes,
|
||||
session_type: s.session_type,
|
||||
status: s.status,
|
||||
rating: s.rating,
|
||||
created_at: s.created_at.to_rfc3339(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(SessionListResponseDto {
|
||||
sessions: items,
|
||||
total: count,
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_user_sessions(
|
||||
&self,
|
||||
user_id: String,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<SessionListResponseDto, AppError> {
|
||||
let user_uuid = Uuid::parse_str(&user_id)
|
||||
.map_err(|e| AppError::BadRequestError(format!("Invalid user ID: {}", e)))?;
|
||||
|
||||
let count = self
|
||||
.repo
|
||||
.count_by_mentee(user_uuid, status_filter.clone())
|
||||
.await?;
|
||||
|
||||
let sessions = self
|
||||
.repo
|
||||
.find_by_mentee_id(user_uuid, status_filter)
|
||||
.await?;
|
||||
|
||||
let items: Vec<SessionListItemDto> = sessions
|
||||
.into_iter()
|
||||
.map(|s| SessionListItemDto {
|
||||
id: s.id.to_string(),
|
||||
mentor_id: s.mentor_id.to_string(),
|
||||
mentee_id: s.mentee_id.to_string(),
|
||||
mentee_fullname: None,
|
||||
mentee_email: None,
|
||||
topic: s.topic,
|
||||
scheduled_at: s.scheduled_at.to_rfc3339(),
|
||||
duration_minutes: s.duration_minutes,
|
||||
session_type: s.session_type,
|
||||
status: s.status,
|
||||
rating: s.rating,
|
||||
created_at: s.created_at.to_rfc3339(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(SessionListResponseDto {
|
||||
sessions: items,
|
||||
total: count,
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_mentor_availability(
|
||||
&self,
|
||||
mentor_id: String,
|
||||
) -> Result<MentorAvailabilityDto, AppError> {
|
||||
let mentor_uuid = Uuid::parse_str(&mentor_id)
|
||||
.map_err(|e| AppError::BadRequestError(format!("Invalid mentor ID: {}", e)))?;
|
||||
|
||||
let booked_dates = self.repo.find_booked_dates(mentor_uuid).await?;
|
||||
|
||||
let mut slots = Vec::new();
|
||||
let today = Utc::now().date_naive();
|
||||
|
||||
for i in 0..7 {
|
||||
let date = today + Duration::days(i);
|
||||
let date_str = date.format("%Y-%m-%d").to_string();
|
||||
|
||||
for hour in 9..17 {
|
||||
let time_str = format!("{:02}:00", hour);
|
||||
let datetime_prefix = format!("{}T{}", date_str, time_str);
|
||||
|
||||
let is_booked = booked_dates
|
||||
.iter()
|
||||
.any(|d| d.starts_with(&datetime_prefix[..13]));
|
||||
|
||||
slots.push(AvailabilitySlotDto {
|
||||
date: date_str.clone(),
|
||||
time: time_str,
|
||||
available: !is_booked,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(MentorAvailabilityDto {
|
||||
mentor_id,
|
||||
availability_commitment: "Available weekdays 9 AM - 5 PM".to_string(),
|
||||
preferred_formats: vec!["video_call".to_string(), "phone_call".to_string()],
|
||||
slots,
|
||||
booked_dates,
|
||||
})
|
||||
}
|
||||
|
||||
async fn update_session_status(
|
||||
&self,
|
||||
session_id: String,
|
||||
_user_id: String,
|
||||
dto: UpdateSessionStatusRequestDto,
|
||||
) -> Result<UpdateSessionStatusResponseDto, AppError> {
|
||||
let session_uuid = Uuid::parse_str(&session_id)
|
||||
.map_err(|e| AppError::BadRequestError(format!("Invalid session ID: {}", e)))?;
|
||||
|
||||
let mut session = self
|
||||
.repo
|
||||
.find_by_id(session_uuid)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::NotFoundError("Session not found".to_string()))?;
|
||||
|
||||
session.status = dto.status;
|
||||
if let Some(link) = dto.meeting_link {
|
||||
session.meeting_link = Some(link);
|
||||
}
|
||||
session.updated_at = Utc::now();
|
||||
|
||||
let updated = self.repo.update(session_uuid, session).await?;
|
||||
|
||||
Ok(UpdateSessionStatusResponseDto {
|
||||
id: updated.id.to_string(),
|
||||
status: updated.status,
|
||||
meeting_link: updated.meeting_link,
|
||||
updated_at: updated.updated_at.to_rfc3339(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn submit_feedback(
|
||||
&self,
|
||||
session_id: String,
|
||||
user_id: String,
|
||||
dto: SessionFeedbackRequestDto,
|
||||
) -> Result<SessionFeedbackResponseDto, AppError> {
|
||||
let session_uuid = Uuid::parse_str(&session_id)
|
||||
.map_err(|e| AppError::BadRequestError(format!("Invalid session ID: {}", e)))?;
|
||||
|
||||
let mut session = self
|
||||
.repo
|
||||
.find_by_id(session_uuid)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::NotFoundError("Session not found".to_string()))?;
|
||||
|
||||
if session.mentee_id.to_string() != user_id {
|
||||
return Err(AppError::ForbiddenError(
|
||||
"Only the mentee can submit feedback".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if session.status != "completed" {
|
||||
return Err(AppError::BadRequestError(
|
||||
"Feedback can only be submitted for completed sessions".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
session.feedback = Some(dto.feedback.clone());
|
||||
session.rating = Some(dto.rating);
|
||||
session.feedback_submitted_at = Some(Utc::now());
|
||||
session.updated_at = Utc::now();
|
||||
|
||||
let updated = self.repo.update(session_uuid, session).await?;
|
||||
let submitted_at = updated
|
||||
.feedback_submitted_at
|
||||
.unwrap_or_else(Utc::now)
|
||||
.to_rfc3339();
|
||||
|
||||
Ok(SessionFeedbackResponseDto {
|
||||
id: updated.id.to_string(),
|
||||
feedback: dto.feedback,
|
||||
rating: dto.rating,
|
||||
submitted_at,
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_session_detail(
|
||||
&self,
|
||||
session_id: String,
|
||||
) -> Result<SessionDetailDto, AppError> {
|
||||
let session_uuid = Uuid::parse_str(&session_id)
|
||||
.map_err(|e| AppError::BadRequestError(format!("Invalid session ID: {}", e)))?;
|
||||
|
||||
let session = self
|
||||
.repo
|
||||
.find_by_id(session_uuid)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::NotFoundError("Session not found".to_string()))?;
|
||||
|
||||
Ok(SessionDetailDto {
|
||||
id: session.id.to_string(),
|
||||
mentor_id: session.mentor_id.to_string(),
|
||||
mentor_fullname: None,
|
||||
mentee_id: session.mentee_id.to_string(),
|
||||
mentee_fullname: None,
|
||||
topic: session.topic,
|
||||
description: session.description,
|
||||
scheduled_at: session.scheduled_at.to_rfc3339(),
|
||||
duration_minutes: session.duration_minutes,
|
||||
meeting_link: session.meeting_link,
|
||||
session_type: session.session_type,
|
||||
status: session.status,
|
||||
feedback: session.feedback,
|
||||
rating: session.rating,
|
||||
feedback_submitted_at: session.feedback_submitted_at.map(|dt| dt.to_rfc3339()),
|
||||
created_at: session.created_at.to_rfc3339(),
|
||||
updated_at: session.updated_at.to_rfc3339(),
|
||||
})
|
||||
}
|
||||
async fn get_session_detail(
|
||||
&self,
|
||||
session_id: String,
|
||||
) -> Result<SessionDetail, AppError> {
|
||||
self.query.get_session_detail(session_id).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
pub mod repository;
|
||||
pub mod service;
|
||||
pub mod session;
|
||||
pub mod session_types;
|
||||
|
||||
pub use repository::SessionRepository;
|
||||
pub use service::SessionService;
|
||||
pub use session::SessionEntity;
|
||||
pub use session_types::{
|
||||
AvailabilitySlot, BookSessionCommand, BookedSession, MentorAvailability,
|
||||
SessionDetail, SessionFeedbackCommand, SessionFeedbackResult, SessionList,
|
||||
SessionListItem, UpdateSessionStatusCommand, UpdatedSessionStatus,
|
||||
};
|
||||
|
||||
@@ -1,48 +1,55 @@
|
||||
use super::session::SessionEntity;
|
||||
use async_trait::async_trait;
|
||||
use imphnen_utils::AppError;
|
||||
use paginator_rs::PaginationParams;
|
||||
use paginator_utils::PaginatorResponse;
|
||||
use uuid::Uuid;
|
||||
use imphnen_utils::AppError;
|
||||
use super::session::SessionEntity;
|
||||
|
||||
#[async_trait]
|
||||
pub trait SessionRepository: Send + Sync {
|
||||
async fn create(&self, entity: SessionEntity) -> Result<SessionEntity, AppError>;
|
||||
async fn create(&self, entity: SessionEntity) -> Result<SessionEntity, AppError>;
|
||||
|
||||
async fn find_by_id(&self, id: Uuid) -> Result<Option<SessionEntity>, AppError>;
|
||||
async fn find_by_id(&self, id: Uuid) -> Result<Option<SessionEntity>, AppError>;
|
||||
|
||||
async fn find_by_mentor_id(
|
||||
&self,
|
||||
mentor_id: Uuid,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<Vec<SessionEntity>, AppError>;
|
||||
async fn find_by_mentor_id(
|
||||
&self,
|
||||
mentor_id: Uuid,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<Vec<SessionEntity>, AppError>;
|
||||
|
||||
async fn find_by_mentee_id(
|
||||
&self,
|
||||
mentee_id: Uuid,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<Vec<SessionEntity>, AppError>;
|
||||
async fn find_by_mentee_id(
|
||||
&self,
|
||||
mentee_id: Uuid,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<Vec<SessionEntity>, AppError>;
|
||||
|
||||
async fn find_booked_dates(&self, mentor_id: Uuid) -> Result<Vec<String>, AppError>;
|
||||
async fn find_booked_dates(
|
||||
&self,
|
||||
mentor_id: Uuid,
|
||||
) -> Result<Vec<String>, AppError>;
|
||||
|
||||
async fn update(&self, id: Uuid, entity: SessionEntity) -> Result<SessionEntity, AppError>;
|
||||
async fn update(
|
||||
&self,
|
||||
id: Uuid,
|
||||
entity: SessionEntity,
|
||||
) -> Result<SessionEntity, AppError>;
|
||||
|
||||
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
|
||||
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
|
||||
|
||||
async fn count_by_mentor(
|
||||
&self,
|
||||
mentor_id: Uuid,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<usize, AppError>;
|
||||
async fn count_by_mentor(
|
||||
&self,
|
||||
mentor_id: Uuid,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<usize, AppError>;
|
||||
|
||||
async fn count_by_mentee(
|
||||
&self,
|
||||
mentee_id: Uuid,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<usize, AppError>;
|
||||
async fn count_by_mentee(
|
||||
&self,
|
||||
mentee_id: Uuid,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<usize, AppError>;
|
||||
|
||||
async fn find_all_paginated(
|
||||
&self,
|
||||
params: PaginationParams,
|
||||
) -> Result<PaginatorResponse<SessionEntity>, AppError>;
|
||||
async fn find_all_paginated(
|
||||
&self,
|
||||
params: PaginationParams,
|
||||
) -> Result<PaginatorResponse<SessionEntity>, AppError>;
|
||||
}
|
||||
|
||||
@@ -1,53 +1,53 @@
|
||||
use super::session_types::{
|
||||
BookSessionCommand, BookedSession, MentorAvailability, SessionDetail,
|
||||
SessionFeedbackCommand, SessionFeedbackResult, SessionList,
|
||||
UpdateSessionStatusCommand, UpdatedSessionStatus,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use imphnen_utils::AppError;
|
||||
use crate::sessions::infrastructure::http::dto::{
|
||||
BookSessionRequestDto, BookSessionResponseDto, MentorAvailabilityDto,
|
||||
SessionDetailDto, SessionFeedbackRequestDto, SessionFeedbackResponseDto,
|
||||
SessionListResponseDto, UpdateSessionStatusRequestDto, UpdateSessionStatusResponseDto,
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
pub trait SessionService: Send + Sync {
|
||||
async fn book_session(
|
||||
&self,
|
||||
mentor_id: String,
|
||||
user_id: String,
|
||||
dto: BookSessionRequestDto,
|
||||
) -> Result<BookSessionResponseDto, AppError>;
|
||||
async fn book_session(
|
||||
&self,
|
||||
mentor_id: String,
|
||||
user_id: String,
|
||||
cmd: BookSessionCommand,
|
||||
) -> Result<BookedSession, AppError>;
|
||||
|
||||
async fn get_mentor_sessions(
|
||||
&self,
|
||||
mentor_id: String,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<SessionListResponseDto, AppError>;
|
||||
async fn get_mentor_sessions(
|
||||
&self,
|
||||
mentor_id: String,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<SessionList, AppError>;
|
||||
|
||||
async fn get_user_sessions(
|
||||
&self,
|
||||
user_id: String,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<SessionListResponseDto, AppError>;
|
||||
async fn get_user_sessions(
|
||||
&self,
|
||||
user_id: String,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<SessionList, AppError>;
|
||||
|
||||
async fn get_mentor_availability(
|
||||
&self,
|
||||
mentor_id: String,
|
||||
) -> Result<MentorAvailabilityDto, AppError>;
|
||||
async fn get_mentor_availability(
|
||||
&self,
|
||||
mentor_id: String,
|
||||
) -> Result<MentorAvailability, AppError>;
|
||||
|
||||
async fn update_session_status(
|
||||
&self,
|
||||
session_id: String,
|
||||
user_id: String,
|
||||
dto: UpdateSessionStatusRequestDto,
|
||||
) -> Result<UpdateSessionStatusResponseDto, AppError>;
|
||||
async fn update_session_status(
|
||||
&self,
|
||||
session_id: String,
|
||||
user_id: String,
|
||||
cmd: UpdateSessionStatusCommand,
|
||||
) -> Result<UpdatedSessionStatus, AppError>;
|
||||
|
||||
async fn submit_feedback(
|
||||
&self,
|
||||
session_id: String,
|
||||
user_id: String,
|
||||
dto: SessionFeedbackRequestDto,
|
||||
) -> Result<SessionFeedbackResponseDto, AppError>;
|
||||
async fn submit_feedback(
|
||||
&self,
|
||||
session_id: String,
|
||||
user_id: String,
|
||||
cmd: SessionFeedbackCommand,
|
||||
) -> Result<SessionFeedbackResult, AppError>;
|
||||
|
||||
async fn get_session_detail(
|
||||
&self,
|
||||
session_id: String,
|
||||
) -> Result<SessionDetailDto, AppError>;
|
||||
async fn get_session_detail(
|
||||
&self,
|
||||
session_id: String,
|
||||
) -> Result<SessionDetail, AppError>;
|
||||
}
|
||||
|
||||
@@ -3,19 +3,19 @@ use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SessionEntity {
|
||||
pub id: Uuid,
|
||||
pub mentor_id: Uuid,
|
||||
pub mentee_id: Uuid,
|
||||
pub topic: String,
|
||||
pub description: Option<String>,
|
||||
pub scheduled_at: DateTime<Utc>,
|
||||
pub duration_minutes: i32,
|
||||
pub meeting_link: Option<String>,
|
||||
pub session_type: String,
|
||||
pub status: String,
|
||||
pub feedback: Option<String>,
|
||||
pub rating: Option<i32>,
|
||||
pub feedback_submitted_at: Option<DateTime<Utc>>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub id: Uuid,
|
||||
pub mentor_id: Uuid,
|
||||
pub mentee_id: Uuid,
|
||||
pub topic: String,
|
||||
pub description: Option<String>,
|
||||
pub scheduled_at: DateTime<Utc>,
|
||||
pub duration_minutes: i32,
|
||||
pub meeting_link: Option<String>,
|
||||
pub session_type: String,
|
||||
pub status: String,
|
||||
pub feedback: Option<String>,
|
||||
pub rating: Option<i32>,
|
||||
pub feedback_submitted_at: Option<DateTime<Utc>>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
pub struct BookSessionCommand {
|
||||
pub topic: String,
|
||||
pub description: Option<String>,
|
||||
pub scheduled_at: String,
|
||||
pub duration_minutes: Option<i32>,
|
||||
pub session_type: Option<String>,
|
||||
}
|
||||
|
||||
pub struct BookedSession {
|
||||
pub id: String,
|
||||
pub mentor_id: String,
|
||||
pub mentee_id: String,
|
||||
pub topic: String,
|
||||
pub description: Option<String>,
|
||||
pub scheduled_at: String,
|
||||
pub duration_minutes: i32,
|
||||
pub session_type: String,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
pub struct SessionListItem {
|
||||
pub id: String,
|
||||
pub mentor_id: String,
|
||||
pub mentee_id: String,
|
||||
pub mentee_fullname: Option<String>,
|
||||
pub mentee_email: Option<String>,
|
||||
pub topic: String,
|
||||
pub scheduled_at: String,
|
||||
pub duration_minutes: i32,
|
||||
pub session_type: String,
|
||||
pub status: String,
|
||||
pub rating: Option<i32>,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
pub struct SessionList {
|
||||
pub sessions: Vec<SessionListItem>,
|
||||
pub total: usize,
|
||||
}
|
||||
|
||||
pub struct SessionDetail {
|
||||
pub id: String,
|
||||
pub mentor_id: String,
|
||||
pub mentor_fullname: Option<String>,
|
||||
pub mentee_id: String,
|
||||
pub mentee_fullname: Option<String>,
|
||||
pub topic: String,
|
||||
pub description: Option<String>,
|
||||
pub scheduled_at: String,
|
||||
pub duration_minutes: i32,
|
||||
pub meeting_link: Option<String>,
|
||||
pub session_type: String,
|
||||
pub status: String,
|
||||
pub feedback: Option<String>,
|
||||
pub rating: Option<i32>,
|
||||
pub feedback_submitted_at: Option<String>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
pub struct AvailabilitySlot {
|
||||
pub date: String,
|
||||
pub time: String,
|
||||
pub available: bool,
|
||||
}
|
||||
|
||||
pub struct MentorAvailability {
|
||||
pub mentor_id: String,
|
||||
pub availability_commitment: String,
|
||||
pub preferred_formats: Vec<String>,
|
||||
pub slots: Vec<AvailabilitySlot>,
|
||||
pub booked_dates: Vec<String>,
|
||||
}
|
||||
|
||||
pub struct UpdateSessionStatusCommand {
|
||||
pub status: String,
|
||||
pub meeting_link: Option<String>,
|
||||
}
|
||||
|
||||
pub struct UpdatedSessionStatus {
|
||||
pub id: String,
|
||||
pub status: String,
|
||||
pub meeting_link: Option<String>,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
pub struct SessionFeedbackCommand {
|
||||
pub feedback: String,
|
||||
pub rating: i32,
|
||||
}
|
||||
|
||||
pub struct SessionFeedbackResult {
|
||||
pub id: String,
|
||||
pub feedback: String,
|
||||
pub rating: i32,
|
||||
pub submitted_at: String,
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
use imphnen_libs::ZodValidate;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use zod_rs::prelude::*;
|
||||
|
||||
// ============================================================
|
||||
// Request DTOs
|
||||
// ============================================================
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
pub struct BookSessionRequestDto {
|
||||
#[zod(min_length(3), max_length(200))]
|
||||
pub topic: String,
|
||||
#[zod(max_length(1000))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
#[zod(min_length(1))]
|
||||
pub scheduled_at: String,
|
||||
#[zod(min(15.0), max(240.0), int)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub duration_minutes: Option<i32>,
|
||||
#[zod(max_length(50))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub session_type: Option<String>,
|
||||
}
|
||||
|
||||
impl ZodValidate for BookSessionRequestDto {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
pub struct UpdateSessionStatusRequestDto {
|
||||
#[zod(min_length(1), max_length(50))]
|
||||
pub status: String,
|
||||
#[zod(url)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub meeting_link: Option<String>,
|
||||
}
|
||||
|
||||
impl ZodValidate for UpdateSessionStatusRequestDto {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
pub struct SessionFeedbackRequestDto {
|
||||
#[zod(min_length(10), max_length(2000))]
|
||||
pub feedback: String,
|
||||
#[zod(min(1.0), max(5.0), int)]
|
||||
pub rating: i32,
|
||||
}
|
||||
|
||||
impl ZodValidate for SessionFeedbackRequestDto {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Response DTOs
|
||||
// ============================================================
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct BookSessionResponseDto {
|
||||
pub id: String,
|
||||
pub mentor_id: String,
|
||||
pub mentee_id: String,
|
||||
pub topic: String,
|
||||
pub description: Option<String>,
|
||||
pub scheduled_at: String,
|
||||
pub duration_minutes: i32,
|
||||
pub session_type: String,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SessionListItemDto {
|
||||
pub id: String,
|
||||
pub mentor_id: String,
|
||||
pub mentee_id: String,
|
||||
pub mentee_fullname: Option<String>,
|
||||
pub mentee_email: Option<String>,
|
||||
pub topic: String,
|
||||
pub scheduled_at: String,
|
||||
pub duration_minutes: i32,
|
||||
pub session_type: String,
|
||||
pub status: String,
|
||||
pub rating: Option<i32>,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SessionListResponseDto {
|
||||
pub sessions: Vec<SessionListItemDto>,
|
||||
pub total: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SessionDetailDto {
|
||||
pub id: String,
|
||||
pub mentor_id: String,
|
||||
pub mentor_fullname: Option<String>,
|
||||
pub mentee_id: String,
|
||||
pub mentee_fullname: Option<String>,
|
||||
pub topic: String,
|
||||
pub description: Option<String>,
|
||||
pub scheduled_at: String,
|
||||
pub duration_minutes: i32,
|
||||
pub meeting_link: Option<String>,
|
||||
pub session_type: String,
|
||||
pub status: String,
|
||||
pub feedback: Option<String>,
|
||||
pub rating: Option<i32>,
|
||||
pub feedback_submitted_at: Option<String>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct AvailabilitySlotDto {
|
||||
pub date: String,
|
||||
pub time: String,
|
||||
pub available: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct MentorAvailabilityDto {
|
||||
pub mentor_id: String,
|
||||
pub availability_commitment: String,
|
||||
pub preferred_formats: Vec<String>,
|
||||
pub slots: Vec<AvailabilitySlotDto>,
|
||||
pub booked_dates: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct UpdateSessionStatusResponseDto {
|
||||
pub id: String,
|
||||
pub status: String,
|
||||
pub meeting_link: Option<String>,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SessionFeedbackResponseDto {
|
||||
pub id: String,
|
||||
pub feedback: String,
|
||||
pub rating: i32,
|
||||
pub submitted_at: String,
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
pub mod request;
|
||||
pub mod response;
|
||||
|
||||
pub use request::{
|
||||
BookSessionRequestDto, SessionFeedbackRequestDto, UpdateSessionStatusRequestDto,
|
||||
};
|
||||
pub use response::{
|
||||
AvailabilitySlotDto, BookSessionResponseDto, MentorAvailabilityDto,
|
||||
SessionDetailDto, SessionFeedbackResponseDto, SessionListItemDto,
|
||||
SessionListResponseDto, UpdateSessionStatusResponseDto,
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
use crate::sessions::domain::{
|
||||
BookSessionCommand, SessionFeedbackCommand, UpdateSessionStatusCommand,
|
||||
};
|
||||
use imphnen_libs::ZodValidate;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use zod_rs::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
pub struct BookSessionRequestDto {
|
||||
#[zod(min_length(3), max_length(200))]
|
||||
pub topic: String,
|
||||
#[zod(max_length(1000))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
#[zod(min_length(1))]
|
||||
pub scheduled_at: String,
|
||||
#[zod(min(15.0), max(240.0), int)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub duration_minutes: Option<i32>,
|
||||
#[zod(max_length(50))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub session_type: Option<String>,
|
||||
}
|
||||
|
||||
impl ZodValidate for BookSessionRequestDto {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BookSessionRequestDto> for BookSessionCommand {
|
||||
fn from(dto: BookSessionRequestDto) -> Self {
|
||||
Self {
|
||||
topic: dto.topic,
|
||||
description: dto.description,
|
||||
scheduled_at: dto.scheduled_at,
|
||||
duration_minutes: dto.duration_minutes,
|
||||
session_type: dto.session_type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
pub struct UpdateSessionStatusRequestDto {
|
||||
#[zod(min_length(1), max_length(50))]
|
||||
pub status: String,
|
||||
#[zod(url)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub meeting_link: Option<String>,
|
||||
}
|
||||
|
||||
impl ZodValidate for UpdateSessionStatusRequestDto {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UpdateSessionStatusRequestDto> for UpdateSessionStatusCommand {
|
||||
fn from(dto: UpdateSessionStatusRequestDto) -> Self {
|
||||
Self {
|
||||
status: dto.status,
|
||||
meeting_link: dto.meeting_link,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, ZodSchema)]
|
||||
pub struct SessionFeedbackRequestDto {
|
||||
#[zod(min_length(10), max_length(2000))]
|
||||
pub feedback: String,
|
||||
#[zod(min(1.0), max(5.0), int)]
|
||||
pub rating: i32,
|
||||
}
|
||||
|
||||
impl ZodValidate for SessionFeedbackRequestDto {
|
||||
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
|
||||
Self::validate_and_parse(value).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SessionFeedbackRequestDto> for SessionFeedbackCommand {
|
||||
fn from(dto: SessionFeedbackRequestDto) -> Self {
|
||||
Self {
|
||||
feedback: dto.feedback,
|
||||
rating: dto.rating,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
use crate::sessions::domain::{
|
||||
AvailabilitySlot, BookedSession, MentorAvailability, SessionDetail,
|
||||
SessionFeedbackResult, SessionList, SessionListItem, UpdatedSessionStatus,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct BookSessionResponseDto {
|
||||
pub id: String,
|
||||
pub mentor_id: String,
|
||||
pub mentee_id: String,
|
||||
pub topic: String,
|
||||
pub description: Option<String>,
|
||||
pub scheduled_at: String,
|
||||
pub duration_minutes: i32,
|
||||
pub session_type: String,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
impl From<BookedSession> for BookSessionResponseDto {
|
||||
fn from(s: BookedSession) -> Self {
|
||||
Self {
|
||||
id: s.id,
|
||||
mentor_id: s.mentor_id,
|
||||
mentee_id: s.mentee_id,
|
||||
topic: s.topic,
|
||||
description: s.description,
|
||||
scheduled_at: s.scheduled_at,
|
||||
duration_minutes: s.duration_minutes,
|
||||
session_type: s.session_type,
|
||||
status: s.status,
|
||||
created_at: s.created_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SessionListItemDto {
|
||||
pub id: String,
|
||||
pub mentor_id: String,
|
||||
pub mentee_id: String,
|
||||
pub mentee_fullname: Option<String>,
|
||||
pub mentee_email: Option<String>,
|
||||
pub topic: String,
|
||||
pub scheduled_at: String,
|
||||
pub duration_minutes: i32,
|
||||
pub session_type: String,
|
||||
pub status: String,
|
||||
pub rating: Option<i32>,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
impl From<SessionListItem> for SessionListItemDto {
|
||||
fn from(s: SessionListItem) -> Self {
|
||||
Self {
|
||||
id: s.id,
|
||||
mentor_id: s.mentor_id,
|
||||
mentee_id: s.mentee_id,
|
||||
mentee_fullname: s.mentee_fullname,
|
||||
mentee_email: s.mentee_email,
|
||||
topic: s.topic,
|
||||
scheduled_at: s.scheduled_at,
|
||||
duration_minutes: s.duration_minutes,
|
||||
session_type: s.session_type,
|
||||
status: s.status,
|
||||
rating: s.rating,
|
||||
created_at: s.created_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SessionListResponseDto {
|
||||
pub sessions: Vec<SessionListItemDto>,
|
||||
pub total: usize,
|
||||
}
|
||||
|
||||
impl From<SessionList> for SessionListResponseDto {
|
||||
fn from(list: SessionList) -> Self {
|
||||
Self {
|
||||
sessions: list
|
||||
.sessions
|
||||
.into_iter()
|
||||
.map(SessionListItemDto::from)
|
||||
.collect(),
|
||||
total: list.total,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SessionDetailDto {
|
||||
pub id: String,
|
||||
pub mentor_id: String,
|
||||
pub mentor_fullname: Option<String>,
|
||||
pub mentee_id: String,
|
||||
pub mentee_fullname: Option<String>,
|
||||
pub topic: String,
|
||||
pub description: Option<String>,
|
||||
pub scheduled_at: String,
|
||||
pub duration_minutes: i32,
|
||||
pub meeting_link: Option<String>,
|
||||
pub session_type: String,
|
||||
pub status: String,
|
||||
pub feedback: Option<String>,
|
||||
pub rating: Option<i32>,
|
||||
pub feedback_submitted_at: Option<String>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl From<SessionDetail> for SessionDetailDto {
|
||||
fn from(d: SessionDetail) -> Self {
|
||||
Self {
|
||||
id: d.id,
|
||||
mentor_id: d.mentor_id,
|
||||
mentor_fullname: d.mentor_fullname,
|
||||
mentee_id: d.mentee_id,
|
||||
mentee_fullname: d.mentee_fullname,
|
||||
topic: d.topic,
|
||||
description: d.description,
|
||||
scheduled_at: d.scheduled_at,
|
||||
duration_minutes: d.duration_minutes,
|
||||
meeting_link: d.meeting_link,
|
||||
session_type: d.session_type,
|
||||
status: d.status,
|
||||
feedback: d.feedback,
|
||||
rating: d.rating,
|
||||
feedback_submitted_at: d.feedback_submitted_at,
|
||||
created_at: d.created_at,
|
||||
updated_at: d.updated_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct AvailabilitySlotDto {
|
||||
pub date: String,
|
||||
pub time: String,
|
||||
pub available: bool,
|
||||
}
|
||||
|
||||
impl From<AvailabilitySlot> for AvailabilitySlotDto {
|
||||
fn from(s: AvailabilitySlot) -> Self {
|
||||
Self {
|
||||
date: s.date,
|
||||
time: s.time,
|
||||
available: s.available,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct MentorAvailabilityDto {
|
||||
pub mentor_id: String,
|
||||
pub availability_commitment: String,
|
||||
pub preferred_formats: Vec<String>,
|
||||
pub slots: Vec<AvailabilitySlotDto>,
|
||||
pub booked_dates: Vec<String>,
|
||||
}
|
||||
|
||||
impl From<MentorAvailability> for MentorAvailabilityDto {
|
||||
fn from(a: MentorAvailability) -> Self {
|
||||
Self {
|
||||
mentor_id: a.mentor_id,
|
||||
availability_commitment: a.availability_commitment,
|
||||
preferred_formats: a.preferred_formats,
|
||||
slots: a.slots.into_iter().map(AvailabilitySlotDto::from).collect(),
|
||||
booked_dates: a.booked_dates,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct UpdateSessionStatusResponseDto {
|
||||
pub id: String,
|
||||
pub status: String,
|
||||
pub meeting_link: Option<String>,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl From<UpdatedSessionStatus> for UpdateSessionStatusResponseDto {
|
||||
fn from(u: UpdatedSessionStatus) -> Self {
|
||||
Self {
|
||||
id: u.id,
|
||||
status: u.status,
|
||||
meeting_link: u.meeting_link,
|
||||
updated_at: u.updated_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SessionFeedbackResponseDto {
|
||||
pub id: String,
|
||||
pub feedback: String,
|
||||
pub rating: i32,
|
||||
pub submitted_at: String,
|
||||
}
|
||||
|
||||
impl From<SessionFeedbackResult> for SessionFeedbackResponseDto {
|
||||
fn from(r: SessionFeedbackResult) -> Self {
|
||||
Self {
|
||||
id: r.id,
|
||||
feedback: r.feedback,
|
||||
rating: r.rating,
|
||||
submitted_at: r.submitted_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
use axum::{
|
||||
extract::{Extension, Path, Query},
|
||||
http::HeaderMap,
|
||||
response::IntoResponse,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use imphnen_libs::ValidatedJson;
|
||||
use imphnen_utils::{ApiSuccess, extract_email};
|
||||
use imphnen_utils::AppError;
|
||||
use crate::sessions::domain::SessionService;
|
||||
use super::dto::{
|
||||
BookSessionRequestDto, BookSessionResponseDto, MentorAvailabilityDto,
|
||||
SessionFeedbackRequestDto, SessionFeedbackResponseDto, SessionListResponseDto,
|
||||
UpdateSessionStatusRequestDto, UpdateSessionStatusResponseDto,
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SessionStatusFilter {
|
||||
pub status: Option<String>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/mentors/{id}/sessions/create",
|
||||
tag = "sessions",
|
||||
security(("Bearer" = [])),
|
||||
params(
|
||||
("id" = String, Path, description = "Mentor ID"),
|
||||
),
|
||||
request_body = BookSessionRequestDto,
|
||||
responses(
|
||||
(status = 201, description = "Session booked successfully", body = BookSessionResponseDto),
|
||||
(status = 400, description = "Invalid request"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 404, description = "Mentor not found"),
|
||||
)
|
||||
)]
|
||||
pub async fn post_book_session(
|
||||
headers: HeaderMap,
|
||||
Extension(service): Extension<Arc<dyn SessionService>>,
|
||||
Path(mentor_id): Path<String>,
|
||||
ValidatedJson(dto): ValidatedJson<BookSessionRequestDto>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let user_email = extract_email(&headers)
|
||||
.ok_or_else(|| AppError::AuthenticationError("Token tidak valid".to_string()))?;
|
||||
let resp = service.book_session(mentor_id, user_email, dto).await?;
|
||||
Ok(ApiSuccess(resp))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/mentors/{id}/sessions",
|
||||
tag = "sessions",
|
||||
security(("Bearer" = [])),
|
||||
params(
|
||||
("id" = String, Path, description = "Mentor ID"),
|
||||
("status" = Option<String>, Query, description = "Filter by status"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Sessions retrieved successfully", body = SessionListResponseDto),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 404, description = "Mentor not found"),
|
||||
)
|
||||
)]
|
||||
pub async fn get_mentor_sessions(
|
||||
headers: HeaderMap,
|
||||
Extension(service): Extension<Arc<dyn SessionService>>,
|
||||
Path(mentor_id): Path<String>,
|
||||
Query(filter): Query<SessionStatusFilter>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let _user_email = extract_email(&headers)
|
||||
.ok_or_else(|| AppError::AuthenticationError("Token tidak valid".to_string()))?;
|
||||
let resp = service.get_mentor_sessions(mentor_id, filter.status).await?;
|
||||
Ok(ApiSuccess(resp))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/mentors/{id}/availability",
|
||||
tag = "sessions",
|
||||
params(
|
||||
("id" = String, Path, description = "Mentor ID"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Availability retrieved successfully", body = MentorAvailabilityDto),
|
||||
(status = 404, description = "Mentor not found"),
|
||||
)
|
||||
)]
|
||||
pub async fn get_mentor_availability(
|
||||
Extension(service): Extension<Arc<dyn SessionService>>,
|
||||
Path(mentor_id): Path<String>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let resp = service.get_mentor_availability(mentor_id).await?;
|
||||
Ok(ApiSuccess(resp))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/v1/sessions/update/{id}/status",
|
||||
tag = "sessions",
|
||||
security(("Bearer" = [])),
|
||||
params(
|
||||
("id" = String, Path, description = "Session ID"),
|
||||
),
|
||||
request_body = UpdateSessionStatusRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "Status updated successfully", body = UpdateSessionStatusResponseDto),
|
||||
(status = 400, description = "Invalid request"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 404, description = "Session not found"),
|
||||
)
|
||||
)]
|
||||
pub async fn put_update_session_status(
|
||||
headers: HeaderMap,
|
||||
Extension(service): Extension<Arc<dyn SessionService>>,
|
||||
Path(session_id): Path<String>,
|
||||
ValidatedJson(dto): ValidatedJson<UpdateSessionStatusRequestDto>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let user_email = extract_email(&headers)
|
||||
.ok_or_else(|| AppError::AuthenticationError("Token tidak valid".to_string()))?;
|
||||
let resp = service.update_session_status(session_id, user_email, dto).await?;
|
||||
Ok(ApiSuccess(resp))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/sessions/{id}/feedback/create",
|
||||
tag = "sessions",
|
||||
security(("Bearer" = [])),
|
||||
params(
|
||||
("id" = String, Path, description = "Session ID"),
|
||||
),
|
||||
request_body = SessionFeedbackRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "Feedback submitted successfully", body = SessionFeedbackResponseDto),
|
||||
(status = 400, description = "Invalid request or session not completed"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Forbidden"),
|
||||
(status = 404, description = "Session not found"),
|
||||
)
|
||||
)]
|
||||
pub async fn post_submit_feedback(
|
||||
headers: HeaderMap,
|
||||
Extension(service): Extension<Arc<dyn SessionService>>,
|
||||
Path(session_id): Path<String>,
|
||||
ValidatedJson(dto): ValidatedJson<SessionFeedbackRequestDto>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let user_email = extract_email(&headers)
|
||||
.ok_or_else(|| AppError::AuthenticationError("Token tidak valid".to_string()))?;
|
||||
let resp = service.submit_feedback(session_id, user_email, dto).await?;
|
||||
Ok(ApiSuccess(resp))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/users/me/sessions",
|
||||
tag = "sessions",
|
||||
security(("Bearer" = [])),
|
||||
params(
|
||||
("status" = Option<String>, Query, description = "Filter by status"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Sessions retrieved successfully", body = SessionListResponseDto),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
)
|
||||
)]
|
||||
pub async fn get_my_sessions(
|
||||
headers: HeaderMap,
|
||||
Extension(service): Extension<Arc<dyn SessionService>>,
|
||||
Query(filter): Query<SessionStatusFilter>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let user_email = extract_email(&headers)
|
||||
.ok_or_else(|| AppError::AuthenticationError("Token tidak valid".to_string()))?;
|
||||
let resp = service.get_user_sessions(user_email, filter.status).await?;
|
||||
Ok(ApiSuccess(resp))
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
pub mod mutation_handlers;
|
||||
pub mod query_handlers;
|
||||
|
||||
pub use mutation_handlers::{
|
||||
post_book_session, post_submit_feedback, put_update_session_status,
|
||||
};
|
||||
pub use query_handlers::{
|
||||
get_mentor_availability, get_mentor_sessions, get_my_sessions,
|
||||
};
|
||||
@@ -0,0 +1,112 @@
|
||||
use super::super::dto::{
|
||||
BookSessionRequestDto, BookSessionResponseDto, SessionFeedbackRequestDto,
|
||||
SessionFeedbackResponseDto, UpdateSessionStatusRequestDto,
|
||||
UpdateSessionStatusResponseDto,
|
||||
};
|
||||
use crate::sessions::domain::SessionService;
|
||||
use axum::{
|
||||
extract::{Extension, Path},
|
||||
http::HeaderMap,
|
||||
response::IntoResponse,
|
||||
};
|
||||
use imphnen_libs::ValidatedJson;
|
||||
use imphnen_utils::AppError;
|
||||
use imphnen_utils::{ApiSuccess, extract_email};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/mentors/{id}/sessions/create",
|
||||
tag = "sessions",
|
||||
security(("Bearer" = [])),
|
||||
params(
|
||||
("id" = String, Path, description = "Mentor ID"),
|
||||
),
|
||||
request_body = BookSessionRequestDto,
|
||||
responses(
|
||||
(status = 201, description = "Session booked successfully", body = BookSessionResponseDto),
|
||||
(status = 400, description = "Invalid request"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 404, description = "Mentor not found"),
|
||||
)
|
||||
)]
|
||||
pub async fn post_book_session(
|
||||
headers: HeaderMap,
|
||||
Extension(service): Extension<Arc<dyn SessionService>>,
|
||||
Path(mentor_id): Path<String>,
|
||||
ValidatedJson(dto): ValidatedJson<BookSessionRequestDto>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let user_email = extract_email(&headers)
|
||||
.ok_or_else(|| AppError::AuthenticationError("Token tidak valid".to_string()))?;
|
||||
let resp = BookSessionResponseDto::from(
|
||||
service
|
||||
.book_session(mentor_id, user_email, dto.into())
|
||||
.await?,
|
||||
);
|
||||
Ok(ApiSuccess(resp))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/v1/sessions/update/{id}/status",
|
||||
tag = "sessions",
|
||||
security(("Bearer" = [])),
|
||||
params(
|
||||
("id" = String, Path, description = "Session ID"),
|
||||
),
|
||||
request_body = UpdateSessionStatusRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "Status updated successfully", body = UpdateSessionStatusResponseDto),
|
||||
(status = 400, description = "Invalid request"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 404, description = "Session not found"),
|
||||
)
|
||||
)]
|
||||
pub async fn put_update_session_status(
|
||||
headers: HeaderMap,
|
||||
Extension(service): Extension<Arc<dyn SessionService>>,
|
||||
Path(session_id): Path<String>,
|
||||
ValidatedJson(dto): ValidatedJson<UpdateSessionStatusRequestDto>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let user_email = extract_email(&headers)
|
||||
.ok_or_else(|| AppError::AuthenticationError("Token tidak valid".to_string()))?;
|
||||
let resp = UpdateSessionStatusResponseDto::from(
|
||||
service
|
||||
.update_session_status(session_id, user_email, dto.into())
|
||||
.await?,
|
||||
);
|
||||
Ok(ApiSuccess(resp))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/sessions/{id}/feedback/create",
|
||||
tag = "sessions",
|
||||
security(("Bearer" = [])),
|
||||
params(
|
||||
("id" = String, Path, description = "Session ID"),
|
||||
),
|
||||
request_body = SessionFeedbackRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "Feedback submitted successfully", body = SessionFeedbackResponseDto),
|
||||
(status = 400, description = "Invalid request or session not completed"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Forbidden"),
|
||||
(status = 404, description = "Session not found"),
|
||||
)
|
||||
)]
|
||||
pub async fn post_submit_feedback(
|
||||
headers: HeaderMap,
|
||||
Extension(service): Extension<Arc<dyn SessionService>>,
|
||||
Path(session_id): Path<String>,
|
||||
ValidatedJson(dto): ValidatedJson<SessionFeedbackRequestDto>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let user_email = extract_email(&headers)
|
||||
.ok_or_else(|| AppError::AuthenticationError("Token tidak valid".to_string()))?;
|
||||
let resp = SessionFeedbackResponseDto::from(
|
||||
service
|
||||
.submit_feedback(session_id, user_email, dto.into())
|
||||
.await?,
|
||||
);
|
||||
Ok(ApiSuccess(resp))
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
use super::super::dto::{MentorAvailabilityDto, SessionListResponseDto};
|
||||
use crate::sessions::domain::SessionService;
|
||||
use axum::{
|
||||
extract::{Extension, Path, Query},
|
||||
http::HeaderMap,
|
||||
response::IntoResponse,
|
||||
};
|
||||
use imphnen_utils::AppError;
|
||||
use imphnen_utils::{ApiSuccess, extract_email};
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SessionStatusFilter {
|
||||
pub status: Option<String>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/mentors/{id}/sessions",
|
||||
tag = "sessions",
|
||||
security(("Bearer" = [])),
|
||||
params(
|
||||
("id" = String, Path, description = "Mentor ID"),
|
||||
("status" = Option<String>, Query, description = "Filter by status"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Sessions retrieved successfully", body = SessionListResponseDto),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 404, description = "Mentor not found"),
|
||||
)
|
||||
)]
|
||||
pub async fn get_mentor_sessions(
|
||||
headers: HeaderMap,
|
||||
Extension(service): Extension<Arc<dyn SessionService>>,
|
||||
Path(mentor_id): Path<String>,
|
||||
Query(filter): Query<SessionStatusFilter>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let _user_email = extract_email(&headers)
|
||||
.ok_or_else(|| AppError::AuthenticationError("Token tidak valid".to_string()))?;
|
||||
let resp = SessionListResponseDto::from(
|
||||
service
|
||||
.get_mentor_sessions(mentor_id, filter.status)
|
||||
.await?,
|
||||
);
|
||||
Ok(ApiSuccess(resp))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/mentors/{id}/availability",
|
||||
tag = "sessions",
|
||||
params(
|
||||
("id" = String, Path, description = "Mentor ID"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Availability retrieved successfully", body = MentorAvailabilityDto),
|
||||
(status = 404, description = "Mentor not found"),
|
||||
)
|
||||
)]
|
||||
pub async fn get_mentor_availability(
|
||||
Extension(service): Extension<Arc<dyn SessionService>>,
|
||||
Path(mentor_id): Path<String>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let resp =
|
||||
MentorAvailabilityDto::from(service.get_mentor_availability(mentor_id).await?);
|
||||
Ok(ApiSuccess(resp))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/users/me/sessions",
|
||||
tag = "sessions",
|
||||
security(("Bearer" = [])),
|
||||
params(
|
||||
("status" = Option<String>, Query, description = "Filter by status"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Sessions retrieved successfully", body = SessionListResponseDto),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
)
|
||||
)]
|
||||
pub async fn get_my_sessions(
|
||||
headers: HeaderMap,
|
||||
Extension(service): Extension<Arc<dyn SessionService>>,
|
||||
Query(filter): Query<SessionStatusFilter>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let user_email = extract_email(&headers)
|
||||
.ok_or_else(|| AppError::AuthenticationError("Token tidak valid".to_string()))?;
|
||||
let resp = SessionListResponseDto::from(
|
||||
service.get_user_sessions(user_email, filter.status).await?,
|
||||
);
|
||||
Ok(ApiSuccess(resp))
|
||||
}
|
||||
@@ -1,38 +1,44 @@
|
||||
use std::sync::Arc;
|
||||
use axum::{
|
||||
routing::{get, post, put},
|
||||
Extension, Router,
|
||||
use super::handlers::{
|
||||
get_mentor_availability, get_mentor_sessions, get_my_sessions, post_book_session,
|
||||
post_submit_feedback, put_update_session_status,
|
||||
};
|
||||
use sea_orm::DatabaseConnection;
|
||||
use imphnen_libs::AppState;
|
||||
use crate::sessions::application::SessionServiceImpl;
|
||||
use crate::sessions::domain::SessionService;
|
||||
use crate::sessions::infrastructure::persistence::PostgresSessionRepository;
|
||||
use super::handlers::{
|
||||
get_mentor_availability, get_mentor_sessions, get_my_sessions, post_book_session,
|
||||
post_submit_feedback, put_update_session_status,
|
||||
use axum::{
|
||||
Extension, Router,
|
||||
routing::{get, post, put},
|
||||
};
|
||||
use imphnen_libs::AppState;
|
||||
use sea_orm::DatabaseConnection;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn build_service(db: DatabaseConnection) -> Arc<dyn SessionService> {
|
||||
let repo = Arc::new(PostgresSessionRepository::new(db));
|
||||
Arc::new(SessionServiceImpl::new(repo))
|
||||
let repo = Arc::new(PostgresSessionRepository::new(db));
|
||||
Arc::new(SessionServiceImpl::new(repo))
|
||||
}
|
||||
|
||||
pub fn sessions_public_routes(db: DatabaseConnection) -> Router {
|
||||
let service = build_service(db);
|
||||
Router::new()
|
||||
.route("/mentors/{id}/availability", get(get_mentor_availability))
|
||||
.layer(Extension(service))
|
||||
let service = build_service(db);
|
||||
Router::new()
|
||||
.route("/mentors/{id}/availability", get(get_mentor_availability))
|
||||
.layer(Extension(service))
|
||||
}
|
||||
|
||||
pub fn sessions_protected_routes(db: DatabaseConnection, state: Arc<AppState>) -> Router {
|
||||
let service = build_service(db);
|
||||
Router::new()
|
||||
.route("/mentors/{id}/sessions/create", post(post_book_session))
|
||||
.route("/mentors/{id}/sessions", get(get_mentor_sessions))
|
||||
.route("/sessions/update/{id}/status", put(put_update_session_status))
|
||||
.route("/sessions/{id}/feedback/create", post(post_submit_feedback))
|
||||
.route("/users/me/sessions", get(get_my_sessions))
|
||||
.layer(Extension(service))
|
||||
.layer(Extension((*state).clone()))
|
||||
pub fn sessions_protected_routes(
|
||||
db: DatabaseConnection,
|
||||
state: Arc<AppState>,
|
||||
) -> Router {
|
||||
let service = build_service(db);
|
||||
Router::new()
|
||||
.route("/mentors/{id}/sessions/create", post(post_book_session))
|
||||
.route("/mentors/{id}/sessions", get(get_mentor_sessions))
|
||||
.route(
|
||||
"/sessions/update/{id}/status",
|
||||
put(put_update_session_status),
|
||||
)
|
||||
.route("/sessions/{id}/feedback/create", post(post_submit_feedback))
|
||||
.route("/users/me/sessions", get(get_my_sessions))
|
||||
.layer(Extension(service))
|
||||
.layer(Extension((*state).clone()))
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod postgres_session_queries;
|
||||
pub mod postgres_session_repository;
|
||||
|
||||
pub use postgres_session_repository::PostgresSessionRepository;
|
||||
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
use super::postgres_session_repository::model_to_entity;
|
||||
use crate::sessions::domain::session::SessionEntity;
|
||||
use imphnen_entities::seaorm::auth::sessions::{
|
||||
Column as SessionColumn, Entity as SessionsEntity,
|
||||
};
|
||||
use imphnen_utils::AppError;
|
||||
use paginator_rs::{PaginationParams, SortDirection};
|
||||
use paginator_utils::{PaginatorResponse, PaginatorResponseMeta};
|
||||
use sea_orm::prelude::*;
|
||||
use sea_orm::{Order, PaginatorTrait, QueryOrder};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub async fn find_by_mentor_id(
|
||||
db: &Arc<DatabaseConnection>,
|
||||
mentor_id: Uuid,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<Vec<SessionEntity>, AppError> {
|
||||
let mut query = SessionsEntity::find()
|
||||
.filter(SessionColumn::MentorId.eq(mentor_id))
|
||||
.order_by(SessionColumn::ScheduledAt, Order::Desc);
|
||||
|
||||
if let Some(status) = status_filter {
|
||||
query = query.filter(SessionColumn::Status.eq(status));
|
||||
}
|
||||
|
||||
let models = query
|
||||
.all(db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
Ok(models.into_iter().map(model_to_entity).collect())
|
||||
}
|
||||
|
||||
pub async fn find_by_mentee_id(
|
||||
db: &Arc<DatabaseConnection>,
|
||||
mentee_id: Uuid,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<Vec<SessionEntity>, AppError> {
|
||||
let mut query = SessionsEntity::find()
|
||||
.filter(SessionColumn::MenteeId.eq(mentee_id))
|
||||
.order_by(SessionColumn::ScheduledAt, Order::Desc);
|
||||
|
||||
if let Some(status) = status_filter {
|
||||
query = query.filter(SessionColumn::Status.eq(status));
|
||||
}
|
||||
|
||||
let models = query
|
||||
.all(db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
Ok(models.into_iter().map(model_to_entity).collect())
|
||||
}
|
||||
|
||||
pub async fn find_booked_dates(
|
||||
db: &Arc<DatabaseConnection>,
|
||||
mentor_id: Uuid,
|
||||
) -> Result<Vec<String>, AppError> {
|
||||
let sessions = SessionsEntity::find()
|
||||
.filter(SessionColumn::MentorId.eq(mentor_id))
|
||||
.filter(SessionColumn::Status.is_in(["pending", "confirmed"]))
|
||||
.order_by(SessionColumn::ScheduledAt, Order::Asc)
|
||||
.all(db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
Ok(
|
||||
sessions
|
||||
.into_iter()
|
||||
.map(|s| s.scheduled_at.to_rfc3339())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn count_by_mentor(
|
||||
db: &Arc<DatabaseConnection>,
|
||||
mentor_id: Uuid,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<usize, AppError> {
|
||||
let mut query =
|
||||
SessionsEntity::find().filter(SessionColumn::MentorId.eq(mentor_id));
|
||||
|
||||
if let Some(status) = status_filter {
|
||||
query = query.filter(SessionColumn::Status.eq(status));
|
||||
}
|
||||
|
||||
let count = query
|
||||
.count(db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
Ok(count as usize)
|
||||
}
|
||||
|
||||
pub async fn count_by_mentee(
|
||||
db: &Arc<DatabaseConnection>,
|
||||
mentee_id: Uuid,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<usize, AppError> {
|
||||
let mut query =
|
||||
SessionsEntity::find().filter(SessionColumn::MenteeId.eq(mentee_id));
|
||||
|
||||
if let Some(status) = status_filter {
|
||||
query = query.filter(SessionColumn::Status.eq(status));
|
||||
}
|
||||
|
||||
let count = query
|
||||
.count(db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
Ok(count as usize)
|
||||
}
|
||||
|
||||
pub async fn find_all_paginated(
|
||||
db: &Arc<DatabaseConnection>,
|
||||
params: PaginationParams,
|
||||
) -> Result<PaginatorResponse<SessionEntity>, AppError> {
|
||||
let page = params.page.max(1);
|
||||
let per_page = params.per_page.clamp(1, 100);
|
||||
|
||||
let query = match params.sort_direction {
|
||||
Some(SortDirection::Asc) => {
|
||||
SessionsEntity::find().order_by(SessionColumn::CreatedAt, Order::Asc)
|
||||
}
|
||||
_ => SessionsEntity::find().order_by(SessionColumn::CreatedAt, Order::Desc),
|
||||
};
|
||||
|
||||
let paginator = query.paginate(db.as_ref(), per_page as u64);
|
||||
let total = paginator
|
||||
.num_items()
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
let sessions = paginator
|
||||
.fetch_page((page - 1) as u64)
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
let data = sessions.into_iter().map(model_to_entity).collect();
|
||||
let meta = PaginatorResponseMeta::new(page, per_page, total as u32);
|
||||
Ok(PaginatorResponse { data, meta })
|
||||
}
|
||||
+153
-224
@@ -1,255 +1,184 @@
|
||||
use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use sea_orm::prelude::*;
|
||||
use sea_orm::{ActiveValue, IntoActiveModel, Order, QueryOrder, PaginatorTrait};
|
||||
use paginator_rs::{PaginationParams, SortDirection};
|
||||
use paginator_utils::{PaginatorResponse, PaginatorResponseMeta};
|
||||
use uuid::Uuid;
|
||||
use imphnen_utils::AppError;
|
||||
use imphnen_entities::seaorm::auth::sessions::{
|
||||
Entity as SessionsEntity,
|
||||
Column as SessionColumn,
|
||||
ActiveModel as SessionActiveModel,
|
||||
Model as SessionModel,
|
||||
use super::postgres_session_queries;
|
||||
use crate::sessions::domain::{
|
||||
repository::SessionRepository, session::SessionEntity,
|
||||
};
|
||||
use crate::sessions::domain::{session::SessionEntity, repository::SessionRepository};
|
||||
use async_trait::async_trait;
|
||||
use imphnen_entities::seaorm::auth::sessions::{
|
||||
ActiveModel as SessionActiveModel, Entity as SessionsEntity, Model as SessionModel,
|
||||
};
|
||||
use imphnen_utils::AppError;
|
||||
use paginator_rs::PaginationParams;
|
||||
use paginator_utils::PaginatorResponse;
|
||||
use sea_orm::prelude::*;
|
||||
use sea_orm::{ActiveValue, IntoActiveModel};
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
fn model_to_entity(model: SessionModel) -> SessionEntity {
|
||||
SessionEntity {
|
||||
id: model.id,
|
||||
mentor_id: model.mentor_id,
|
||||
mentee_id: model.mentee_id,
|
||||
topic: model.topic,
|
||||
description: model.description,
|
||||
scheduled_at: model.scheduled_at,
|
||||
duration_minutes: model.duration_minutes,
|
||||
meeting_link: model.meeting_link,
|
||||
session_type: model.session_type,
|
||||
status: model.status,
|
||||
feedback: model.feedback,
|
||||
rating: model.rating,
|
||||
feedback_submitted_at: model.feedback_submitted_at,
|
||||
created_at: model.created_at,
|
||||
updated_at: model.updated_at,
|
||||
}
|
||||
pub fn model_to_entity(model: SessionModel) -> SessionEntity {
|
||||
SessionEntity {
|
||||
id: model.id,
|
||||
mentor_id: model.mentor_id,
|
||||
mentee_id: model.mentee_id,
|
||||
topic: model.topic,
|
||||
description: model.description,
|
||||
scheduled_at: model.scheduled_at,
|
||||
duration_minutes: model.duration_minutes,
|
||||
meeting_link: model.meeting_link,
|
||||
session_type: model.session_type,
|
||||
status: model.status,
|
||||
feedback: model.feedback,
|
||||
rating: model.rating,
|
||||
feedback_submitted_at: model.feedback_submitted_at,
|
||||
created_at: model.created_at,
|
||||
updated_at: model.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PostgresSessionRepository {
|
||||
db: Arc<DatabaseConnection>,
|
||||
pub db: Arc<DatabaseConnection>,
|
||||
}
|
||||
|
||||
impl PostgresSessionRepository {
|
||||
pub fn new(db: DatabaseConnection) -> Self {
|
||||
Self { db: Arc::new(db) }
|
||||
}
|
||||
pub fn new(db: DatabaseConnection) -> Self {
|
||||
Self { db: Arc::new(db) }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SessionRepository for PostgresSessionRepository {
|
||||
async fn create(&self, entity: SessionEntity) -> Result<SessionEntity, AppError> {
|
||||
let active_model = SessionActiveModel {
|
||||
id: ActiveValue::Set(entity.id),
|
||||
mentor_id: ActiveValue::Set(entity.mentor_id),
|
||||
mentee_id: ActiveValue::Set(entity.mentee_id),
|
||||
topic: ActiveValue::Set(entity.topic.clone()),
|
||||
description: ActiveValue::Set(entity.description.clone()),
|
||||
scheduled_at: ActiveValue::Set(entity.scheduled_at),
|
||||
duration_minutes: ActiveValue::Set(entity.duration_minutes),
|
||||
meeting_link: ActiveValue::Set(entity.meeting_link.clone()),
|
||||
session_type: ActiveValue::Set(entity.session_type.clone()),
|
||||
status: ActiveValue::Set(entity.status.clone()),
|
||||
feedback: ActiveValue::Set(entity.feedback.clone()),
|
||||
rating: ActiveValue::Set(entity.rating),
|
||||
feedback_submitted_at: ActiveValue::Set(entity.feedback_submitted_at),
|
||||
created_at: ActiveValue::Set(entity.created_at),
|
||||
updated_at: ActiveValue::Set(entity.updated_at),
|
||||
};
|
||||
async fn create(&self, entity: SessionEntity) -> Result<SessionEntity, AppError> {
|
||||
let active_model = SessionActiveModel {
|
||||
id: ActiveValue::Set(entity.id),
|
||||
mentor_id: ActiveValue::Set(entity.mentor_id),
|
||||
mentee_id: ActiveValue::Set(entity.mentee_id),
|
||||
topic: ActiveValue::Set(entity.topic.clone()),
|
||||
description: ActiveValue::Set(entity.description.clone()),
|
||||
scheduled_at: ActiveValue::Set(entity.scheduled_at),
|
||||
duration_minutes: ActiveValue::Set(entity.duration_minutes),
|
||||
meeting_link: ActiveValue::Set(entity.meeting_link.clone()),
|
||||
session_type: ActiveValue::Set(entity.session_type.clone()),
|
||||
status: ActiveValue::Set(entity.status.clone()),
|
||||
feedback: ActiveValue::Set(entity.feedback.clone()),
|
||||
rating: ActiveValue::Set(entity.rating),
|
||||
feedback_submitted_at: ActiveValue::Set(entity.feedback_submitted_at),
|
||||
created_at: ActiveValue::Set(entity.created_at),
|
||||
updated_at: ActiveValue::Set(entity.updated_at),
|
||||
};
|
||||
|
||||
let model: SessionModel = active_model
|
||||
.insert(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
let model: SessionModel = active_model
|
||||
.insert(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
Ok(model_to_entity(model))
|
||||
}
|
||||
Ok(model_to_entity(model))
|
||||
}
|
||||
|
||||
async fn find_by_id(&self, id: Uuid) -> Result<Option<SessionEntity>, AppError> {
|
||||
let model = SessionsEntity::find_by_id(id)
|
||||
.one(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
async fn find_by_id(&self, id: Uuid) -> Result<Option<SessionEntity>, AppError> {
|
||||
let model = SessionsEntity::find_by_id(id)
|
||||
.one(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
Ok(model.map(model_to_entity))
|
||||
}
|
||||
Ok(model.map(model_to_entity))
|
||||
}
|
||||
|
||||
async fn find_by_mentor_id(
|
||||
&self,
|
||||
mentor_id: Uuid,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<Vec<SessionEntity>, AppError> {
|
||||
let mut query = SessionsEntity::find()
|
||||
.filter(SessionColumn::MentorId.eq(mentor_id))
|
||||
.order_by(SessionColumn::ScheduledAt, Order::Desc);
|
||||
async fn find_by_mentor_id(
|
||||
&self,
|
||||
mentor_id: Uuid,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<Vec<SessionEntity>, AppError> {
|
||||
postgres_session_queries::find_by_mentor_id(&self.db, mentor_id, status_filter)
|
||||
.await
|
||||
}
|
||||
|
||||
if let Some(status) = status_filter {
|
||||
query = query.filter(SessionColumn::Status.eq(status));
|
||||
}
|
||||
async fn find_by_mentee_id(
|
||||
&self,
|
||||
mentee_id: Uuid,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<Vec<SessionEntity>, AppError> {
|
||||
postgres_session_queries::find_by_mentee_id(&self.db, mentee_id, status_filter)
|
||||
.await
|
||||
}
|
||||
|
||||
let models = query
|
||||
.all(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
async fn find_booked_dates(
|
||||
&self,
|
||||
mentor_id: Uuid,
|
||||
) -> Result<Vec<String>, AppError> {
|
||||
postgres_session_queries::find_booked_dates(&self.db, mentor_id).await
|
||||
}
|
||||
|
||||
Ok(models.into_iter().map(model_to_entity).collect())
|
||||
}
|
||||
async fn update(
|
||||
&self,
|
||||
id: Uuid,
|
||||
entity: SessionEntity,
|
||||
) -> Result<SessionEntity, AppError> {
|
||||
let model = SessionsEntity::find_by_id(id)
|
||||
.one(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||
.ok_or_else(|| AppError::NotFoundError("Session not found".to_string()))?;
|
||||
|
||||
async fn find_by_mentee_id(
|
||||
&self,
|
||||
mentee_id: Uuid,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<Vec<SessionEntity>, AppError> {
|
||||
let mut query = SessionsEntity::find()
|
||||
.filter(SessionColumn::MenteeId.eq(mentee_id))
|
||||
.order_by(SessionColumn::ScheduledAt, Order::Desc);
|
||||
let mut active_model = model.into_active_model();
|
||||
|
||||
if let Some(status) = status_filter {
|
||||
query = query.filter(SessionColumn::Status.eq(status));
|
||||
}
|
||||
active_model.topic = ActiveValue::Set(entity.topic);
|
||||
active_model.description = ActiveValue::Set(entity.description);
|
||||
active_model.scheduled_at = ActiveValue::Set(entity.scheduled_at);
|
||||
active_model.duration_minutes = ActiveValue::Set(entity.duration_minutes);
|
||||
active_model.meeting_link = ActiveValue::Set(entity.meeting_link);
|
||||
active_model.session_type = ActiveValue::Set(entity.session_type);
|
||||
active_model.status = ActiveValue::Set(entity.status);
|
||||
active_model.feedback = ActiveValue::Set(entity.feedback);
|
||||
active_model.rating = ActiveValue::Set(entity.rating);
|
||||
active_model.feedback_submitted_at =
|
||||
ActiveValue::Set(entity.feedback_submitted_at);
|
||||
active_model.updated_at = ActiveValue::Set(entity.updated_at);
|
||||
|
||||
let models = query
|
||||
.all(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
let updated: SessionModel = active_model
|
||||
.update(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e: sea_orm::DbErr| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
Ok(models.into_iter().map(model_to_entity).collect())
|
||||
}
|
||||
Ok(model_to_entity(updated))
|
||||
}
|
||||
|
||||
async fn find_booked_dates(&self, mentor_id: Uuid) -> Result<Vec<String>, AppError> {
|
||||
let sessions = SessionsEntity::find()
|
||||
.filter(SessionColumn::MentorId.eq(mentor_id))
|
||||
.filter(SessionColumn::Status.is_in(["pending", "confirmed"]))
|
||||
.order_by(SessionColumn::ScheduledAt, Order::Asc)
|
||||
.all(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
|
||||
let model = SessionsEntity::find_by_id(id)
|
||||
.one(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||
.ok_or_else(|| AppError::NotFoundError("Session not found".to_string()))?;
|
||||
|
||||
Ok(sessions
|
||||
.into_iter()
|
||||
.map(|s| s.scheduled_at.to_rfc3339())
|
||||
.collect())
|
||||
}
|
||||
model
|
||||
.into_active_model()
|
||||
.delete(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e: sea_orm::DbErr| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
async fn update(&self, id: Uuid, entity: SessionEntity) -> Result<SessionEntity, AppError> {
|
||||
let model = SessionsEntity::find_by_id(id)
|
||||
.one(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||
.ok_or_else(|| AppError::NotFoundError("Session not found".to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let mut active_model = model.into_active_model();
|
||||
async fn count_by_mentor(
|
||||
&self,
|
||||
mentor_id: Uuid,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<usize, AppError> {
|
||||
postgres_session_queries::count_by_mentor(&self.db, mentor_id, status_filter)
|
||||
.await
|
||||
}
|
||||
|
||||
active_model.topic = ActiveValue::Set(entity.topic);
|
||||
active_model.description = ActiveValue::Set(entity.description);
|
||||
active_model.scheduled_at = ActiveValue::Set(entity.scheduled_at);
|
||||
active_model.duration_minutes = ActiveValue::Set(entity.duration_minutes);
|
||||
active_model.meeting_link = ActiveValue::Set(entity.meeting_link);
|
||||
active_model.session_type = ActiveValue::Set(entity.session_type);
|
||||
active_model.status = ActiveValue::Set(entity.status);
|
||||
active_model.feedback = ActiveValue::Set(entity.feedback);
|
||||
active_model.rating = ActiveValue::Set(entity.rating);
|
||||
active_model.feedback_submitted_at = ActiveValue::Set(entity.feedback_submitted_at);
|
||||
active_model.updated_at = ActiveValue::Set(entity.updated_at);
|
||||
async fn count_by_mentee(
|
||||
&self,
|
||||
mentee_id: Uuid,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<usize, AppError> {
|
||||
postgres_session_queries::count_by_mentee(&self.db, mentee_id, status_filter)
|
||||
.await
|
||||
}
|
||||
|
||||
let updated: SessionModel = active_model
|
||||
.update(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e: sea_orm::DbErr| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
Ok(model_to_entity(updated))
|
||||
}
|
||||
|
||||
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
|
||||
let model = SessionsEntity::find_by_id(id)
|
||||
.one(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||
.ok_or_else(|| AppError::NotFoundError("Session not found".to_string()))?;
|
||||
|
||||
model
|
||||
.into_active_model()
|
||||
.delete(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e: sea_orm::DbErr| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn count_by_mentor(
|
||||
&self,
|
||||
mentor_id: Uuid,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<usize, AppError> {
|
||||
let mut query = SessionsEntity::find()
|
||||
.filter(SessionColumn::MentorId.eq(mentor_id));
|
||||
|
||||
if let Some(status) = status_filter {
|
||||
query = query.filter(SessionColumn::Status.eq(status));
|
||||
}
|
||||
|
||||
let count = query
|
||||
.count(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
Ok(count as usize)
|
||||
}
|
||||
|
||||
async fn count_by_mentee(
|
||||
&self,
|
||||
mentee_id: Uuid,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<usize, AppError> {
|
||||
let mut query = SessionsEntity::find()
|
||||
.filter(SessionColumn::MenteeId.eq(mentee_id));
|
||||
|
||||
if let Some(status) = status_filter {
|
||||
query = query.filter(SessionColumn::Status.eq(status));
|
||||
}
|
||||
|
||||
let count = query
|
||||
.count(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
Ok(count as usize)
|
||||
}
|
||||
|
||||
async fn find_all_paginated(
|
||||
&self,
|
||||
params: PaginationParams,
|
||||
) -> Result<PaginatorResponse<SessionEntity>, AppError> {
|
||||
let page = params.page.max(1);
|
||||
let per_page = params.per_page.clamp(1, 100);
|
||||
|
||||
let query = match params.sort_direction {
|
||||
Some(SortDirection::Asc) => SessionsEntity::find()
|
||||
.order_by(SessionColumn::CreatedAt, Order::Asc),
|
||||
_ => SessionsEntity::find()
|
||||
.order_by(SessionColumn::CreatedAt, Order::Desc),
|
||||
};
|
||||
|
||||
let paginator = query.paginate(self.db.as_ref(), per_page as u64);
|
||||
let total = paginator
|
||||
.num_items()
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
let sessions = paginator
|
||||
.fetch_page((page - 1) as u64)
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
let data = sessions.into_iter().map(model_to_entity).collect();
|
||||
let meta = PaginatorResponseMeta::new(page, per_page, total as u32);
|
||||
Ok(PaginatorResponse { data, meta })
|
||||
}
|
||||
async fn find_all_paginated(
|
||||
&self,
|
||||
params: PaginationParams,
|
||||
) -> Result<PaginatorResponse<SessionEntity>, AppError> {
|
||||
postgres_session_queries::find_all_paginated(&self.db, params).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,6 @@ pub mod application;
|
||||
pub mod domain;
|
||||
pub mod infrastructure;
|
||||
|
||||
pub use infrastructure::http::routes::{sessions_protected_routes, sessions_public_routes};
|
||||
pub use infrastructure::http::routes::{
|
||||
sessions_protected_routes, sessions_public_routes,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user