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:
maulanasdqn
2026-04-02 22:29:08 +07:00
co-authored by Claude Sonnet 4.6
parent 2ae43b3bcc
commit 331a4a4e88
442 changed files with 22226 additions and 18700 deletions
@@ -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;
@@ -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 })
}
@@ -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
}
}
+3 -1
View File
@@ -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,
};