postgress
This commit is contained in:
@@ -1,32 +1,33 @@
|
||||
[package]
|
||||
name = "imphnen-dimentorin"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
imphnen-libs.workspace = true
|
||||
imphnen-utils.workspace = true
|
||||
imphnen-entities.workspace = true
|
||||
imphnen-iam.workspace = true
|
||||
imphnen-middleware.workspace = true
|
||||
axum.workspace = true
|
||||
async-trait.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
utoipa.workspace = true
|
||||
lazy_static.workspace = true
|
||||
regex.workspace = true
|
||||
validator.workspace = true
|
||||
axum-test.workspace = true
|
||||
surrealdb.workspace = true
|
||||
rand.workspace = true
|
||||
tokio.workspace = true
|
||||
chrono.workspace = true
|
||||
anyhow.workspace = true
|
||||
tower-http.workspace = true
|
||||
utoipa-swagger-ui.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
dotenvy.workspace = true
|
||||
http-body-util.workspace = true
|
||||
[package]
|
||||
name = "imphnen-dimentorin"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
imphnen-libs.workspace = true
|
||||
imphnen-utils.workspace = true
|
||||
imphnen-entities.workspace = true
|
||||
imphnen-iam.workspace = true
|
||||
imphnen-middleware.workspace = true
|
||||
axum.workspace = true
|
||||
async-trait.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
utoipa.workspace = true
|
||||
lazy_static.workspace = true
|
||||
regex.workspace = true
|
||||
validator.workspace = true
|
||||
axum-test.workspace = true
|
||||
rand.workspace = true
|
||||
tokio.workspace = true
|
||||
chrono.workspace = true
|
||||
anyhow.workspace = true
|
||||
tower-http.workspace = true
|
||||
utoipa-swagger-ui.workspace = true
|
||||
tracing.workspace = true
|
||||
uuid.workspace = true
|
||||
sea-orm.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
dotenvy.workspace = true
|
||||
http-body-util.workspace = true
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
pub mod v1;
|
||||
|
||||
// Explicitly export only what's needed from v1
|
||||
pub use v1::dimentorin_router;
|
||||
pub use v1::mentors::mentors_router;
|
||||
pub use v1::sessions::{
|
||||
sessions_router, BookSessionRequestDto, BookSessionResponseDto, MentorAvailabilityDto,
|
||||
SessionFeedbackRequestDto, SessionFeedbackResponseDto, SessionListItemDto,
|
||||
SessionListResponseDto, UpdateSessionStatusRequestDto, UpdateSessionStatusResponseDto,
|
||||
AvailabilitySlotDto,
|
||||
};
|
||||
|
||||
pub mod v1;
|
||||
|
||||
// Explicitly export only what's needed from v1
|
||||
pub use v1::dimentorin_router;
|
||||
pub use v1::mentors::mentors_router;
|
||||
pub use v1::sessions::{
|
||||
sessions_router, BookSessionRequestDto, BookSessionResponseDto, MentorAvailabilityDto,
|
||||
SessionFeedbackRequestDto, SessionFeedbackResponseDto, SessionListItemDto,
|
||||
SessionListResponseDto, UpdateSessionStatusRequestDto, UpdateSessionStatusResponseDto,
|
||||
AvailabilitySlotDto,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,286 +1,333 @@
|
||||
use super::{
|
||||
MentorDetailResponseDto, MentorListResponseDto, MentorUpdateRequestDto,
|
||||
MentorUserRegisterRequestDto, MentorVerifyRequestDto, MentorsService,
|
||||
};
|
||||
use crate::v1::mentors::mentors_dto::MentorRegisterResponseDto;
|
||||
use ::axum::{
|
||||
extract::{Extension, Path, Query},
|
||||
http::HeaderMap,
|
||||
response::Response,
|
||||
};
|
||||
use imphnen_entities::MetaRequestDto;
|
||||
use imphnen_libs::{AppState, ValidatedJson};
|
||||
use imphnen_iam::{PermissionsEnum, require_permissions};
|
||||
use imphnen_utils::extract_email;
|
||||
|
||||
#[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(app_state): Extension<AppState>,
|
||||
ValidatedJson(dto): ValidatedJson<MentorUserRegisterRequestDto>,
|
||||
) -> Response {
|
||||
MentorsService::register_mentor(&app_state, dto).await
|
||||
}
|
||||
|
||||
#[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(app_state): Extension<AppState>,
|
||||
Query(meta): Query<MetaRequestDto>,
|
||||
) -> Response {
|
||||
require_permissions!(headers, app_state, [PermissionsEnum::ReadListMentors], {
|
||||
MentorsService::get_mentor_list(&app_state, meta).await
|
||||
})
|
||||
}
|
||||
|
||||
#[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(app_state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> Response {
|
||||
require_permissions!(headers, app_state, [PermissionsEnum::ReadDetailMentors], {
|
||||
MentorsService::get_mentor_by_id(&app_state, &id).await
|
||||
})
|
||||
}
|
||||
|
||||
#[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(app_state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
ValidatedJson(dto): ValidatedJson<MentorUpdateRequestDto>,
|
||||
) -> Response {
|
||||
require_permissions!(headers, app_state, [PermissionsEnum::UpdateMentors], {
|
||||
MentorsService::update_mentor(&app_state, &id, dto).await
|
||||
})
|
||||
}
|
||||
|
||||
#[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(app_state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> Response {
|
||||
require_permissions!(headers, app_state, [PermissionsEnum::DeleteMentors], {
|
||||
MentorsService::delete_mentor(&app_state, &id).await
|
||||
})
|
||||
}
|
||||
|
||||
#[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(app_state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
ValidatedJson(dto): ValidatedJson<MentorVerifyRequestDto>,
|
||||
) -> Response {
|
||||
require_permissions!(headers, app_state, [PermissionsEnum::VerifyMentors], {
|
||||
MentorsService::verify_mentor(&app_state, &id, dto).await
|
||||
})
|
||||
}
|
||||
|
||||
#[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(app_state): Extension<AppState>,
|
||||
) -> Response {
|
||||
require_permissions!(headers.clone(), app_state, [PermissionsEnum::ReadOwnMentorProfile], {
|
||||
let email = match extract_email(&headers) {
|
||||
Some(email) => email,
|
||||
None => {
|
||||
return imphnen_utils::common_response(
|
||||
axum::http::StatusCode::UNAUTHORIZED,
|
||||
"Token tidak valid",
|
||||
);
|
||||
}
|
||||
};
|
||||
MentorsService::get_mentor_me(&app_state, &email).await
|
||||
})
|
||||
}
|
||||
|
||||
#[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(app_state): Extension<AppState>,
|
||||
ValidatedJson(dto): ValidatedJson<MentorUpdateRequestDto>,
|
||||
) -> Response {
|
||||
require_permissions!(headers.clone(), app_state, [PermissionsEnum::UpdateOwnMentorProfile], {
|
||||
let email = match extract_email(&headers) {
|
||||
Some(email) => email,
|
||||
None => {
|
||||
return imphnen_utils::common_response(
|
||||
axum::http::StatusCode::UNAUTHORIZED,
|
||||
"Token tidak valid",
|
||||
);
|
||||
}
|
||||
};
|
||||
MentorsService::update_mentor_me(&app_state, &email, dto).await
|
||||
})
|
||||
}
|
||||
#[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() -> Response {
|
||||
imphnen_utils::common_response(
|
||||
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(app_state): Extension<AppState>,
|
||||
) -> Response {
|
||||
require_permissions!(headers.clone(), app_state, [PermissionsEnum::ReadOwnMentorStatus], {
|
||||
let email = match extract_email(&headers) {
|
||||
Some(email) => email,
|
||||
None => {
|
||||
return imphnen_utils::common_response(
|
||||
axum::http::StatusCode::UNAUTHORIZED,
|
||||
"Token tidak valid",
|
||||
);
|
||||
}
|
||||
};
|
||||
MentorsService::get_mentor_status(&app_state, &email).await
|
||||
})
|
||||
}
|
||||
use super::{
|
||||
MentorDetailResponseDto, MentorListResponseDto, MentorUpdateRequestDto,
|
||||
MentorUserRegisterRequestDto, MentorVerifyRequestDto, MentorsService,
|
||||
};
|
||||
use crate::v1::mentors::mentors_dto::MentorRegisterResponseDto;
|
||||
use ::axum::{
|
||||
extract::{Extension, Path, Query},
|
||||
http::HeaderMap,
|
||||
response::Response,
|
||||
};
|
||||
use imphnen_entities::MetaRequestDto;
|
||||
use uuid::Uuid;
|
||||
use axum::http::StatusCode;
|
||||
use imphnen_utils::common_response;
|
||||
use imphnen_libs::{AppState, ValidatedJson};
|
||||
use imphnen_iam::{PermissionsEnum, require_permissions};
|
||||
use imphnen_utils::extract_email;
|
||||
|
||||
#[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(app_state): Extension<AppState>,
|
||||
ValidatedJson(dto): ValidatedJson<MentorUserRegisterRequestDto>,
|
||||
) -> Response {
|
||||
MentorsService::register_mentor(&app_state, dto).await
|
||||
}
|
||||
|
||||
#[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(app_state): Extension<AppState>,
|
||||
Query(meta): Query<MetaRequestDto>,
|
||||
) -> Response {
|
||||
require_permissions!(headers, app_state, [PermissionsEnum::ReadListMentors], {
|
||||
MentorsService::get_mentor_list(&app_state, meta).await
|
||||
})
|
||||
}
|
||||
|
||||
#[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(app_state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> Response {
|
||||
// Validate UUID format
|
||||
let _mentor_uuid = match Uuid::parse_str(&id) {
|
||||
Ok(uuid) => uuid,
|
||||
Err(_) => {
|
||||
return common_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Invalid mentor ID format. Must be a valid UUID."
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
require_permissions!(headers, app_state, [PermissionsEnum::ReadDetailMentors], {
|
||||
MentorsService::get_mentor_by_id(&app_state, &id).await
|
||||
})
|
||||
}
|
||||
|
||||
#[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(app_state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
ValidatedJson(dto): ValidatedJson<MentorUpdateRequestDto>,
|
||||
) -> Response {
|
||||
// Validate UUID format
|
||||
let _mentor_uuid = match Uuid::parse_str(&id) {
|
||||
Ok(uuid) => uuid,
|
||||
Err(_) => {
|
||||
return common_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Invalid mentor ID format. Must be a valid UUID."
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
require_permissions!(headers, app_state, [PermissionsEnum::UpdateMentors], {
|
||||
MentorsService::update_mentor(&app_state, &id, dto).await
|
||||
})
|
||||
}
|
||||
|
||||
#[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(app_state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> Response {
|
||||
// Validate UUID format
|
||||
let _mentor_uuid = match Uuid::parse_str(&id) {
|
||||
Ok(uuid) => uuid,
|
||||
Err(_) => {
|
||||
return common_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Invalid mentor ID format. Must be a valid UUID."
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
require_permissions!(headers, app_state, [PermissionsEnum::DeleteMentors], {
|
||||
MentorsService::delete_mentor(&app_state, &id).await
|
||||
})
|
||||
}
|
||||
|
||||
#[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(app_state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
ValidatedJson(dto): ValidatedJson<MentorVerifyRequestDto>,
|
||||
) -> Response {
|
||||
// Validate UUID format
|
||||
let _mentor_uuid = match Uuid::parse_str(&id) {
|
||||
Ok(uuid) => uuid,
|
||||
Err(_) => {
|
||||
return common_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Invalid mentor ID format. Must be a valid UUID."
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
require_permissions!(headers, app_state, [PermissionsEnum::VerifyMentors], {
|
||||
MentorsService::verify_mentor(&app_state, &id, dto).await
|
||||
})
|
||||
}
|
||||
|
||||
#[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(app_state): Extension<AppState>,
|
||||
) -> Response {
|
||||
require_permissions!(headers.clone(), app_state, [PermissionsEnum::ReadOwnMentorProfile], {
|
||||
let email = match extract_email(&headers) {
|
||||
Some(email) => email,
|
||||
None => {
|
||||
return imphnen_utils::common_response(
|
||||
axum::http::StatusCode::UNAUTHORIZED,
|
||||
"Token tidak valid",
|
||||
);
|
||||
}
|
||||
};
|
||||
MentorsService::get_mentor_me(&app_state, &email).await
|
||||
})
|
||||
}
|
||||
|
||||
#[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(app_state): Extension<AppState>,
|
||||
ValidatedJson(dto): ValidatedJson<MentorUpdateRequestDto>,
|
||||
) -> Response {
|
||||
require_permissions!(headers.clone(), app_state, [PermissionsEnum::UpdateOwnMentorProfile], {
|
||||
let email = match extract_email(&headers) {
|
||||
Some(email) => email,
|
||||
None => {
|
||||
return imphnen_utils::common_response(
|
||||
axum::http::StatusCode::UNAUTHORIZED,
|
||||
"Token tidak valid",
|
||||
);
|
||||
}
|
||||
};
|
||||
MentorsService::update_mentor_me(&app_state, &email, dto).await
|
||||
})
|
||||
}
|
||||
#[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() -> Response {
|
||||
imphnen_utils::common_response(
|
||||
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(app_state): Extension<AppState>,
|
||||
) -> Response {
|
||||
require_permissions!(headers.clone(), app_state, [PermissionsEnum::ReadOwnMentorStatus], {
|
||||
let email = match extract_email(&headers) {
|
||||
Some(email) => email,
|
||||
None => {
|
||||
return imphnen_utils::common_response(
|
||||
axum::http::StatusCode::UNAUTHORIZED,
|
||||
"Token tidak valid",
|
||||
);
|
||||
}
|
||||
};
|
||||
MentorsService::get_mentor_status(&app_state, &email).await
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::v1::mentors::MentorSchema;
|
||||
use imphnen_utils::extract_id;
|
||||
use crate::v1::sessions::sessions_schema::Thing;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::sql::Thing;
|
||||
use utoipa::ToSchema;
|
||||
use validator::Validate;
|
||||
|
||||
@@ -16,8 +15,8 @@ pub struct MentorListResponseDto {
|
||||
}
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct MentorDetailWithUserDto {
|
||||
pub id: Thing,
|
||||
pub user_id: Thing,
|
||||
pub id: String,
|
||||
pub user_id: String,
|
||||
// Personal data is now in UsersSchema, access via user_id
|
||||
// Removed: fullname, email, legal_name, identity_document_url,
|
||||
// phone_for_verification, bio, linkedin_url, github_url, cv_url
|
||||
@@ -31,7 +30,7 @@ pub struct MentorDetailWithUserDto {
|
||||
pub preferred_mentee_level: Vec<String>,
|
||||
pub preferred_mentoring_formats: Vec<String>,
|
||||
pub availability_commitment: String,
|
||||
pub mentoring_rate: MentoringRate,
|
||||
pub mentoring_rate: f64,
|
||||
pub status: String,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: String,
|
||||
@@ -65,7 +64,7 @@ pub struct MentorDetailResponseDto {
|
||||
pub preferred_mentee_level: Vec<String>,
|
||||
pub preferred_mentoring_formats: Vec<String>,
|
||||
pub availability_commitment: String,
|
||||
pub mentoring_rate: MentoringRate,
|
||||
pub mentoring_rate: f64,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
@@ -173,8 +172,8 @@ pub struct MentorUserRegisterRequestDto {
|
||||
pub password: String,
|
||||
#[validate(length(min = 2, message = "Fullname at least have 2 character"))]
|
||||
pub fullname: String,
|
||||
#[validate(length(min = 1, message = "Phone number is required"))]
|
||||
pub phone_number: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub phone_number: Option<String>,
|
||||
#[validate(nested)]
|
||||
pub identity_and_verification: IdentityAndVerification,
|
||||
#[validate(nested)]
|
||||
@@ -211,7 +210,8 @@ pub struct IdentityAndVerification {
|
||||
max = 15,
|
||||
message = "Phone must be 10-15 characters"
|
||||
))]
|
||||
pub phone_for_verification: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub phone_for_verification: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
@@ -287,7 +287,7 @@ pub struct MentorInsertDto {
|
||||
pub preferred_mentee_level: Vec<String>,
|
||||
pub preferred_mentoring_formats: Vec<String>,
|
||||
pub availability_commitment: String,
|
||||
pub mentoring_rate: MentoringRate,
|
||||
pub mentoring_rate: f64,
|
||||
pub status: String,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: String,
|
||||
@@ -343,7 +343,7 @@ pub struct MentorDetailQueryDto {
|
||||
pub preferred_mentee_level: Vec<String>,
|
||||
pub preferred_mentoring_formats: Vec<String>,
|
||||
pub availability_commitment: String,
|
||||
pub mentoring_rate: MentoringRate,
|
||||
pub mentoring_rate: f64,
|
||||
pub status: String,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: String,
|
||||
@@ -353,7 +353,7 @@ pub struct MentorDetailQueryDto {
|
||||
impl From<MentorDetailQueryDto> for MentorListResponseDto {
|
||||
fn from(dto: MentorDetailQueryDto) -> Self {
|
||||
Self {
|
||||
id: extract_id(&dto.id),
|
||||
id: dto.id.clone(),
|
||||
fullname: None, // now in user table, must be populated from service layer
|
||||
email: None, // now in user table, must be populated from service layer
|
||||
status: dto.status,
|
||||
@@ -366,8 +366,8 @@ impl From<MentorDetailQueryDto> for MentorListResponseDto {
|
||||
impl From<MentorDetailQueryDto> for MentorDetailResponseDto {
|
||||
fn from(dto: MentorDetailQueryDto) -> Self {
|
||||
Self {
|
||||
id: extract_id(&dto.id),
|
||||
user_id: extract_id(&dto.user_id),
|
||||
id: dto.id.clone(),
|
||||
user_id: dto.user_id.clone(),
|
||||
// Personal data fields are populated in service layer from UsersSchema
|
||||
fullname: None, // populated from user table in service layer
|
||||
email: None, // populated from user table in service layer
|
||||
@@ -403,7 +403,7 @@ impl From<MentorSchema> for MentorRegisterResponseDto {
|
||||
fn from(schema: MentorSchema) -> Self {
|
||||
Self {
|
||||
id: schema.id.to_string(),
|
||||
user_id: schema.user_id.map(|id| extract_id(&id)).unwrap_or_default(),
|
||||
user_id: schema.user_id.unwrap_or_default(),
|
||||
email: None, // schema.email - now in user table
|
||||
status: schema.status,
|
||||
created_at: schema.created_at,
|
||||
|
||||
@@ -1,296 +1,323 @@
|
||||
use crate::v1::mentors::mentors_schema::MentorSchema;
|
||||
use imphnen_libs::AppState;
|
||||
use imphnen_libs::AppStatePostgresExt;
|
||||
use imphnen_entities::seaorm::auth::mentors::{Entity as Mentors, ActiveModel as MentorActiveModel, Column as MentorColumn};
|
||||
use imphnen_entities::seaorm::auth::users::Entity as Users;
|
||||
use anyhow::{Result, bail};
|
||||
use imphnen_iam::{get_id, make_thing};
|
||||
use surrealdb::sql::Thing;
|
||||
|
||||
use crate::v1::mentors::mentors_dto::MentorDetailWithUserDto;
|
||||
use crate::v1::mentors::{MentorInsertDto, MentorSchema};
|
||||
use imphnen_libs::{AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto};
|
||||
use imphnen_utils::{DetailQueryBuilder, QueryListBuilder, get_iso_date};
|
||||
use serde_json::{Map, Value};
|
||||
use sea_orm::*;
|
||||
use sea_orm::EntityTrait;
|
||||
use uuid::Uuid;
|
||||
use sea_orm::ActiveModelTrait as ActiveModelTraitSpecific;
|
||||
use anyhow::anyhow;
|
||||
use imphnen_entities::{MetaRequestDto, ResponseListSuccessDto};
|
||||
use imphnen_utils::Result as UtilsResult;
|
||||
use crate::v1::mentors::mentors_dto::MentorDetailQueryDto;
|
||||
use std::time::Instant;
|
||||
use tracing::instrument;
|
||||
use tracing::info;
|
||||
use tracing::{instrument, info};
|
||||
use serde_json;
|
||||
use chrono::Utc;
|
||||
|
||||
pub struct MentorsRepository<'a> {
|
||||
pub state: &'a AppState,
|
||||
state: &'a AppState,
|
||||
}
|
||||
|
||||
impl<'a> MentorsRepository<'a> {
|
||||
pub fn new(state: &'a AppState) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
pub fn new(state: &'a AppState) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
|
||||
#[instrument(skip(self, meta), err)]
|
||||
pub async fn query_mentor_list(
|
||||
&self,
|
||||
meta: MetaRequestDto,
|
||||
) -> Result<ResponseListSuccessDto<Vec<MentorDetailWithUserDto>>> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let mentors_table = ResourceEnum::Mentors.to_string();
|
||||
let builder = QueryListBuilder::new(db, &mentors_table, &meta)
|
||||
.search_field("user_id.legal_name") // Search in user data instead
|
||||
.select_fields(vec![
|
||||
"id",
|
||||
"user_id",
|
||||
// Personal data comes from user relation, not mentor table
|
||||
"industries",
|
||||
"expertise",
|
||||
"languages",
|
||||
"current_company",
|
||||
"current_role",
|
||||
"years_of_experience",
|
||||
"topics_of_interest",
|
||||
"preferred_mentee_level",
|
||||
"preferred_mentoring_formats",
|
||||
"availability_commitment",
|
||||
"mentoring_rate",
|
||||
"status",
|
||||
"is_deleted",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]);
|
||||
let result = builder.build().await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_mentor_list' took: {elapsed:.2?}");
|
||||
}
|
||||
let data = result.data.into_iter().collect();
|
||||
Ok(ResponseListSuccessDto {
|
||||
data,
|
||||
meta: result.meta,
|
||||
})
|
||||
}
|
||||
fn get_db(&self) -> &DatabaseConnection {
|
||||
self.state.postgres_db()
|
||||
}
|
||||
|
||||
#[instrument(skip(self, email, include_deleted), err)]
|
||||
pub async fn query_mentor_by_email(
|
||||
&self,
|
||||
email: String,
|
||||
include_deleted: bool,
|
||||
) -> Result<MentorDetailWithUserDto> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let mut builder = DetailQueryBuilder::new(ResourceEnum::Mentors.to_string())
|
||||
.with_where("user_id.email", Some(email.clone())) // Search in user table
|
||||
.with_select_fields(vec![
|
||||
"id",
|
||||
"user_id",
|
||||
// Personal data comes from user relation, not mentor table
|
||||
"industries",
|
||||
"expertise",
|
||||
"languages",
|
||||
"current_company",
|
||||
"current_role",
|
||||
"years_of_experience",
|
||||
"topics_of_interest",
|
||||
"preferred_mentee_level",
|
||||
"preferred_mentoring_formats",
|
||||
"availability_commitment",
|
||||
"mentoring_rate",
|
||||
"status",
|
||||
"is_deleted",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]);
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_create_mentor(&self, data: MentorSchema) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
|
||||
let mentor_active_model = MentorActiveModel {
|
||||
user_id: ActiveValue::Set(Uuid::parse_str(&data.user_id.unwrap_or_default())?),
|
||||
industries: ActiveValue::Set(Some(data.industries.into())),
|
||||
expertise: ActiveValue::Set(Some(data.expertise.into())),
|
||||
languages: ActiveValue::Set(Some(data.languages.into())),
|
||||
current_company: ActiveValue::Set(Some(data.current_company)),
|
||||
current_role: ActiveValue::Set(Some(data.current_role)),
|
||||
years_of_experience: ActiveValue::Set(Some(data.years_of_experience)),
|
||||
topics_of_interest: ActiveValue::Set(Some(data.topics_of_interest.into())),
|
||||
preferred_mentee_level: ActiveValue::Set(Some(serde_json::to_string(&data.preferred_mentee_level).unwrap())),
|
||||
preferred_mentoring_formats: ActiveValue::Set(Some(data.preferred_mentoring_formats.into())),
|
||||
availability_commitment: ActiveValue::Set(Some(data.availability_commitment)),
|
||||
mentoring_rate: ActiveValue::Set(Some(data.mentoring_rate)),
|
||||
status: ActiveValue::Set(Some(data.status)),
|
||||
created_at: ActiveValue::Set(Utc::now()),
|
||||
updated_at: ActiveValue::Set(Utc::now()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if !include_deleted {
|
||||
builder = builder.with_condition("is_deleted = false");
|
||||
}
|
||||
info!("Executing PostgreSQL create in query_create_mentor");
|
||||
let result = <MentorActiveModel as sea_orm::ActiveModelTrait>::insert(mentor_active_model, self.get_db()).await?;
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) == "development" {
|
||||
println!("Query 'query_create_mentor' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
let sql = builder.build();
|
||||
info!(query = %sql, "Executing SurrealDB query in query_mentor_by_email");
|
||||
let mentor_opt: Option<MentorDetailWithUserDto> =
|
||||
builder.apply_bindings(db.query(sql)).await?.take(0)?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_mentor_by_email' took: {elapsed:.2?}");
|
||||
}
|
||||
let Some(mentor) = mentor_opt else {
|
||||
bail!("Mentor not found");
|
||||
};
|
||||
Ok(mentor)
|
||||
}
|
||||
Ok(result.id.to_string())
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id, include_deleted), err)]
|
||||
pub async fn query_mentor_by_id(
|
||||
&self,
|
||||
id: &Thing,
|
||||
include_deleted: bool,
|
||||
) -> Result<MentorDetailWithUserDto> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_update_mentor(&self, data: MentorSchema) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
|
||||
// Validate ID format first
|
||||
let mentor_id = match get_id(id) {
|
||||
Ok((_, id_str)) => id_str,
|
||||
Err(_) => bail!("Invalid mentor ID format"),
|
||||
};
|
||||
let mentor_id = Uuid::parse_str(&data.id)?;
|
||||
let existing_mentor = Mentors::find_by_id(mentor_id)
|
||||
.one(self.get_db())
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("Mentor not found"))?;
|
||||
|
||||
let mentors_table = ResourceEnum::Mentors.to_string();
|
||||
|
||||
// Build query with proper ID binding
|
||||
let mut builder = DetailQueryBuilder::new(mentors_table.clone())
|
||||
.with_id(mentor_id) // Use the extracted ID string
|
||||
.with_select_fields(vec![
|
||||
"id",
|
||||
"user_id",
|
||||
// Personal data comes from user relation, not mentor table
|
||||
"industries",
|
||||
"expertise",
|
||||
"languages",
|
||||
"current_company",
|
||||
"current_role",
|
||||
"years_of_experience",
|
||||
"topics_of_interest",
|
||||
"preferred_mentee_level",
|
||||
"preferred_mentoring_formats",
|
||||
"availability_commitment",
|
||||
"mentoring_rate",
|
||||
"status",
|
||||
"is_deleted",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]);
|
||||
let mut mentor_active_model: MentorActiveModel = existing_mentor.into();
|
||||
|
||||
if !include_deleted {
|
||||
builder = builder.with_condition("is_deleted = false");
|
||||
}
|
||||
if !data.industries.is_empty() { mentor_active_model.industries = ActiveValue::Set(Some(serde_json::to_value(data.industries).unwrap())); }
|
||||
if !data.expertise.is_empty() { mentor_active_model.expertise = ActiveValue::Set(Some(serde_json::to_value(data.expertise).unwrap())); }
|
||||
if !data.languages.is_empty() { mentor_active_model.languages = ActiveValue::Set(Some(serde_json::to_value(data.languages).unwrap())); }
|
||||
if !data.current_company.is_empty() { mentor_active_model.current_company = ActiveValue::Set(Some(data.current_company)); }
|
||||
if !data.current_role.is_empty() { mentor_active_model.current_role = ActiveValue::Set(Some(data.current_role)); }
|
||||
mentor_active_model.years_of_experience = ActiveValue::Set(Some(data.years_of_experience));
|
||||
if !data.topics_of_interest.is_empty() { mentor_active_model.topics_of_interest = ActiveValue::Set(Some(serde_json::to_value(data.topics_of_interest).unwrap())); }
|
||||
if !data.preferred_mentee_level.is_empty() { mentor_active_model.preferred_mentee_level = ActiveValue::Set(Some(serde_json::to_string(&data.preferred_mentee_level).unwrap())); }
|
||||
if !data.preferred_mentoring_formats.is_empty() { mentor_active_model.preferred_mentoring_formats = ActiveValue::Set(Some(serde_json::to_value(data.preferred_mentoring_formats).unwrap())); }
|
||||
if !data.availability_commitment.is_empty() { mentor_active_model.availability_commitment = ActiveValue::Set(Some(data.availability_commitment)); }
|
||||
mentor_active_model.mentoring_rate = ActiveValue::Set(Some(data.mentoring_rate));
|
||||
if !data.status.is_empty() { mentor_active_model.status = ActiveValue::Set(Some(data.status)); }
|
||||
mentor_active_model.updated_at = ActiveValue::Set(Utc::now());
|
||||
|
||||
let sql = builder.build();
|
||||
info!(query = %sql, "Executing SurrealDB query in query_mentor_by_id");
|
||||
info!("Executing PostgreSQL update in query_update_mentor");
|
||||
let result = mentor_active_model.update(self.get_db()).await?;
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) == "development" {
|
||||
println!("Query 'query_update_mentor' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
let mentor_opt: Option<MentorDetailWithUserDto> = builder
|
||||
.apply_bindings(db.query(sql))
|
||||
.await?
|
||||
.take(0)?;
|
||||
Ok(format!("Success update mentor: {}", result.id))
|
||||
}
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_mentor_by_id' took: {elapsed:.2?}");
|
||||
}
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_delete_mentor(&self, id: &str) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
|
||||
let Some(mentor) = mentor_opt else {
|
||||
bail!("Mentor not found");
|
||||
};
|
||||
let mentor = Mentors::find_by_id(Uuid::parse_str(id)?)
|
||||
.one(self.get_db())
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("Mentor not found"))?;
|
||||
|
||||
Ok(mentor)
|
||||
}
|
||||
if mentor.is_deleted {
|
||||
bail!("Mentor is already soft deleted");
|
||||
}
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_create_mentor(&self, data: MentorSchema) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let dto: MentorInsertDto = data.into();
|
||||
let resource = ResourceEnum::Mentors.to_string();
|
||||
info!(query = %resource, "Executing SurrealDB create in query_create_mentor");
|
||||
let record: Option<MentorSchema> = db
|
||||
.create(resource)
|
||||
.content(dto.clone())
|
||||
.await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_create_mentor' took: {elapsed:.2?}");
|
||||
}
|
||||
match record {
|
||||
Some(mentor) => {
|
||||
let id_str = mentor.id.id.to_raw();
|
||||
let _user = format!("{:?}", mentor.user_id);
|
||||
Ok(id_str)
|
||||
}
|
||||
None => {
|
||||
bail!("Failed to create mentor")
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut mentor_active_model: MentorActiveModel = mentor.into();
|
||||
mentor_active_model.is_deleted = ActiveValue::Set(true);
|
||||
mentor_active_model.updated_at = ActiveValue::Set(Utc::now());
|
||||
|
||||
#[instrument(skip(self, data), err)]
|
||||
pub async fn query_update_mentor(&self, data: MentorSchema) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let id_ref = &data.id;
|
||||
let record_key = get_id(id_ref)?;
|
||||
let _existing = self.query_mentor_by_id(id_ref, false).await?;
|
||||
info!("Executing PostgreSQL soft delete in query_delete_mentor");
|
||||
let result = mentor_active_model.update(self.get_db()).await?;
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) == "development" {
|
||||
println!("Query 'query_delete_mentor' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
let mut merged_data_json: Map<String, Value> =
|
||||
serde_json::to_value(data.clone())
|
||||
.map_err(|e| anyhow::anyhow!("Failed to serialize MentorSchema: {}", e))?
|
||||
.as_object()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
Ok(format!("Success soft delete mentor: {}", result.id))
|
||||
}
|
||||
|
||||
merged_data_json.remove("id");
|
||||
merged_data_json.remove("user_id");
|
||||
merged_data_json.remove("created_at");
|
||||
#[instrument(skip(self, email, include_deleted), err)]
|
||||
pub async fn query_mentor_by_email(
|
||||
&self,
|
||||
email: String,
|
||||
include_deleted: bool,
|
||||
) -> UtilsResult<MentorDetailQueryDto> {
|
||||
let now = Instant::now();
|
||||
let db = self.get_db();
|
||||
|
||||
merged_data_json.insert("updated_at".to_string(), Value::String(get_iso_date()));
|
||||
let mut query = Mentors::find()
|
||||
.find_also_related(Users);
|
||||
|
||||
info!(query = ?record_key, "Executing SurrealDB update in query_update_mentor");
|
||||
let record: Option<MentorSchema> =
|
||||
db.update(record_key).merge(merged_data_json).await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_update_mentor' took: {elapsed:.2?}");
|
||||
}
|
||||
match record {
|
||||
Some(_) => Ok("Success update mentor".into()),
|
||||
None => {
|
||||
bail!("Failed to update mentor")
|
||||
}
|
||||
}
|
||||
}
|
||||
if !include_deleted {
|
||||
query = query.filter(MentorColumn::IsDeleted.eq(false));
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id), err)]
|
||||
pub async fn query_delete_mentor(&self, id: String) -> Result<String> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let thing = make_thing(ResourceEnum::Mentors.to_string().as_str(), &id);
|
||||
let record_key = get_id(&thing)?;
|
||||
let (mentor, user) = query
|
||||
.filter(imphnen_entities::seaorm::auth::users::Column::Email.eq(email))
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Mentor not found"))?;
|
||||
|
||||
let mentor_to_delete_res = self.query_mentor_by_id(&thing, true).await;
|
||||
let _user = user.ok_or_else(|| anyhow::anyhow!("User not found for mentor"))?;
|
||||
|
||||
let _mentor_to_delete = match mentor_to_delete_res {
|
||||
Ok(mentor) => {
|
||||
if mentor.is_deleted {
|
||||
bail!("Mentor is already soft deleted");
|
||||
}
|
||||
mentor
|
||||
}
|
||||
Err(e) => {
|
||||
if e.to_string().contains("Mentor has been deleted") {
|
||||
bail!("Mentor is already soft deleted");
|
||||
} else {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
let result = MentorDetailQueryDto {
|
||||
id: mentor.id.to_string(),
|
||||
user_id: mentor.user_id.to_string(),
|
||||
industries: serde_json::from_value(mentor.industries.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
expertise: serde_json::from_value(mentor.expertise.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
languages: serde_json::from_value(mentor.languages.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
current_company: mentor.current_company.unwrap_or_default(),
|
||||
current_role: mentor.current_role.unwrap_or_default(),
|
||||
years_of_experience: mentor.years_of_experience.unwrap_or(0),
|
||||
topics_of_interest: serde_json::from_value(mentor.topics_of_interest.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
preferred_mentee_level: serde_json::from_str(&mentor.preferred_mentee_level.unwrap_or_default()).unwrap_or_default(),
|
||||
preferred_mentoring_formats: serde_json::from_value(mentor.preferred_mentoring_formats.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
availability_commitment: mentor.availability_commitment.unwrap_or_default(),
|
||||
mentoring_rate: mentor.mentoring_rate.unwrap_or(0.0),
|
||||
status: mentor.status.unwrap_or_default(),
|
||||
is_deleted: mentor.is_deleted,
|
||||
created_at: mentor.created_at.to_rfc3339(),
|
||||
updated_at: mentor.updated_at.to_rfc3339(),
|
||||
};
|
||||
|
||||
let mut patch = Map::new();
|
||||
patch.insert("is_deleted".to_string(), Value::Bool(true));
|
||||
patch.insert("updated_at".to_string(), Value::String(get_iso_date()));
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) == "development" {
|
||||
println!("Query 'query_mentor_by_email' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
info!(query = ?record_key, "Executing SurrealDB soft delete in query_delete_mentor");
|
||||
let record: Option<MentorSchema> = db.update(record_key).merge(patch).await?;
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
|
||||
== "development"
|
||||
{
|
||||
println!("Query 'query_delete_mentor' took: {elapsed:.2?}");
|
||||
}
|
||||
match record {
|
||||
Some(_) => Ok("Success soft delete mentor".into()),
|
||||
None => {
|
||||
bail!("Failed to soft delete mentor")
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[instrument(skip(self, meta), err)]
|
||||
pub async fn query_mentor_list(
|
||||
&self,
|
||||
meta: MetaRequestDto,
|
||||
) -> UtilsResult<ResponseListSuccessDto<Vec<MentorDetailQueryDto>>> {
|
||||
let now = Instant::now();
|
||||
let db = self.get_db();
|
||||
|
||||
let page = meta.page.unwrap_or(1);
|
||||
let per_page = meta.per_page.unwrap_or(10);
|
||||
|
||||
let mut query = Mentors::find()
|
||||
.filter(MentorColumn::IsDeleted.eq(false))
|
||||
.find_also_related(Users);
|
||||
|
||||
// Apply sorting
|
||||
if let Some(sort_by) = &meta.sort_by {
|
||||
let order = meta.order.as_deref().unwrap_or("asc");
|
||||
match sort_by.as_str() {
|
||||
"created_at" => {
|
||||
if order == "desc" {
|
||||
query = query.order_by_desc(MentorColumn::CreatedAt);
|
||||
} else {
|
||||
query = query.order_by_asc(MentorColumn::CreatedAt);
|
||||
}
|
||||
}
|
||||
"updated_at" => {
|
||||
if order == "desc" {
|
||||
query = query.order_by_desc(MentorColumn::UpdatedAt);
|
||||
} else {
|
||||
query = query.order_by_asc(MentorColumn::UpdatedAt);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
query = query.order_by_desc(MentorColumn::CreatedAt);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
query = query.order_by_desc(MentorColumn::CreatedAt);
|
||||
}
|
||||
|
||||
let paginator = query.paginate(db, per_page);
|
||||
let total_pages = paginator.num_pages().await?;
|
||||
let mentors = paginator.fetch_page(page - 1).await?;
|
||||
|
||||
let data: Vec<MentorDetailQueryDto> = mentors
|
||||
.into_iter()
|
||||
.filter_map(|(mentor, user)| {
|
||||
user.map(|_u| MentorDetailQueryDto {
|
||||
id: mentor.id.to_string(),
|
||||
user_id: mentor.user_id.to_string(),
|
||||
industries: serde_json::from_value(mentor.industries.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
expertise: serde_json::from_value(mentor.expertise.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
languages: serde_json::from_value(mentor.languages.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
current_company: mentor.current_company.unwrap_or_default(),
|
||||
current_role: mentor.current_role.unwrap_or_default(),
|
||||
years_of_experience: mentor.years_of_experience.unwrap_or(0),
|
||||
topics_of_interest: serde_json::from_value(mentor.topics_of_interest.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
preferred_mentee_level: serde_json::from_str(&mentor.preferred_mentee_level.unwrap_or_default()).unwrap_or_default(),
|
||||
preferred_mentoring_formats: serde_json::from_value(mentor.preferred_mentoring_formats.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
availability_commitment: mentor.availability_commitment.unwrap_or_default(),
|
||||
mentoring_rate: mentor.mentoring_rate.unwrap_or(0.0),
|
||||
status: mentor.status.unwrap_or_default(),
|
||||
is_deleted: mentor.is_deleted,
|
||||
created_at: mentor.created_at.to_rfc3339(),
|
||||
updated_at: mentor.updated_at.to_rfc3339(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) == "development" {
|
||||
println!("Query 'query_mentor_list' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
let response = ResponseListSuccessDto {
|
||||
data,
|
||||
meta: Some(imphnen_entities::MetaResponseDto {
|
||||
page: Some(page),
|
||||
per_page: Some(per_page),
|
||||
total: Some(total_pages),
|
||||
}),
|
||||
};
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id, include_deleted), err)]
|
||||
pub async fn query_mentor_by_id(
|
||||
&self,
|
||||
id: &str,
|
||||
include_deleted: bool,
|
||||
) -> UtilsResult<MentorDetailQueryDto> {
|
||||
let now = Instant::now();
|
||||
let db = self.get_db();
|
||||
|
||||
let mentor_id = Uuid::parse_str(id).map_err(|e| anyhow!("Invalid mentor ID: {}", e))?;
|
||||
|
||||
let mut query = Mentors::find_by_id(mentor_id)
|
||||
.find_also_related(Users);
|
||||
|
||||
if !include_deleted {
|
||||
query = query.filter(MentorColumn::IsDeleted.eq(false));
|
||||
}
|
||||
|
||||
let (mentor, user) = query
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Mentor not found"))?;
|
||||
|
||||
let _user = user.ok_or_else(|| anyhow::anyhow!("User not found for mentor"))?;
|
||||
|
||||
let result = MentorDetailQueryDto {
|
||||
id: mentor.id.to_string(),
|
||||
user_id: mentor.user_id.to_string(),
|
||||
industries: serde_json::from_value(mentor.industries.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
expertise: serde_json::from_value(mentor.expertise.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
languages: serde_json::from_value(mentor.languages.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
current_company: mentor.current_company.unwrap_or_default(),
|
||||
current_role: mentor.current_role.unwrap_or_default(),
|
||||
years_of_experience: mentor.years_of_experience.unwrap_or(0),
|
||||
topics_of_interest: serde_json::from_value(mentor.topics_of_interest.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
preferred_mentee_level: serde_json::from_str(&mentor.preferred_mentee_level.unwrap_or_default()).unwrap_or_default(),
|
||||
preferred_mentoring_formats: serde_json::from_value(mentor.preferred_mentoring_formats.clone().unwrap_or(serde_json::Value::Null)).unwrap_or_default(),
|
||||
availability_commitment: mentor.availability_commitment.unwrap_or_default(),
|
||||
mentoring_rate: mentor.mentoring_rate.unwrap_or(0.0),
|
||||
status: mentor.status.unwrap_or_default(),
|
||||
is_deleted: mentor.is_deleted,
|
||||
created_at: mentor.created_at.to_rfc3339(),
|
||||
updated_at: mentor.updated_at.to_rfc3339(),
|
||||
};
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string()) == "development" {
|
||||
println!("Query 'query_mentor_by_id' took: {elapsed:.2?}");
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use super::{
|
||||
MentorDetailQueryDto, MentorUpdateRequestDto,
|
||||
MentoringLogistics, MentoringRate, ProfessionalProfile,
|
||||
MentoringLogistics, ProfessionalProfile,
|
||||
};
|
||||
use imphnen_libs::ResourceEnum;
|
||||
use crate::v1::sessions::sessions_schema::Thing;
|
||||
use imphnen_entities::ResourceEnum;
|
||||
use imphnen_utils::{get_iso_date, make_thing};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::{Uuid, sql::Thing};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct MentorSchema {
|
||||
@@ -25,7 +26,7 @@ pub struct MentorSchema {
|
||||
pub preferred_mentee_level: Vec<String>,
|
||||
pub preferred_mentoring_formats: Vec<String>,
|
||||
pub availability_commitment: String,
|
||||
pub mentoring_rate: MentoringRate,
|
||||
pub mentoring_rate: f64,
|
||||
pub status: String,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: String,
|
||||
@@ -53,11 +54,7 @@ impl Default for MentorSchema {
|
||||
preferred_mentee_level: Vec::new(),
|
||||
preferred_mentoring_formats: Vec::new(),
|
||||
availability_commitment: String::new(),
|
||||
mentoring_rate: MentoringRate {
|
||||
amount: 0,
|
||||
currency: "IDR".to_string(),
|
||||
per_duration: "hour".to_string(),
|
||||
},
|
||||
mentoring_rate: 0.0,
|
||||
status: "pending".to_string(),
|
||||
is_deleted: false,
|
||||
created_at: get_iso_date(),
|
||||
@@ -89,11 +86,7 @@ impl MentorSchema {
|
||||
preferred_mentee_level: mentoring_logistics.preferred_mentee_level,
|
||||
preferred_mentoring_formats: mentoring_logistics.preferred_mentoring_formats,
|
||||
availability_commitment: mentoring_logistics.availability_commitment,
|
||||
mentoring_rate: MentoringRate {
|
||||
amount: mentoring_logistics.mentoring_rate_amount,
|
||||
currency: "IDR".to_string(),
|
||||
per_duration: "hour".to_string(),
|
||||
},
|
||||
mentoring_rate: mentoring_logistics.mentoring_rate_amount as f64,
|
||||
status: "pending".to_string(),
|
||||
is_deleted: false,
|
||||
created_at: get_iso_date(),
|
||||
@@ -160,7 +153,7 @@ impl MentorSchema {
|
||||
self.availability_commitment = val;
|
||||
}
|
||||
if let Some(val) = dto.mentoring_rate_amount {
|
||||
self.mentoring_rate.amount = val;
|
||||
self.mentoring_rate = val as f64;
|
||||
}
|
||||
|
||||
self.updated_at = get_iso_date();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::v1::mentors::{
|
||||
MentorDetailQueryDto, MentorDetailResponseDto, MentorListResponseDto,
|
||||
MentorDetailResponseDto, MentorListResponseDto,
|
||||
MentorRegisterResponseDto, MentorSchema, MentorUpdateRequestDto,
|
||||
MentorUserRegisterRequestDto, MentorVerifyRequestDto, MentorsRepository,
|
||||
};
|
||||
@@ -10,14 +10,14 @@ use imphnen_entities::{
|
||||
};
|
||||
use imphnen_libs::AppState;
|
||||
use imphnen_iam::{
|
||||
AuthRepository, RolesEnum, RolesRepository, UsersRepository, UsersSchema,
|
||||
v1::auth::AuthRepository,
|
||||
RolesEnum, RolesRepository, UsersRepository, UsersSchema,
|
||||
};
|
||||
use imphnen_libs::ResourceEnum;
|
||||
use imphnen_libs::argon::hash_password;
|
||||
use imphnen_utils::{
|
||||
common_response, success_list_response, success_response, validate_request,
|
||||
common_response, success_list_response, success_response, validator::validate_request,
|
||||
};
|
||||
use surrealdb::Uuid;
|
||||
use surrealdb::sql::Thing;
|
||||
use uuid::Uuid;
|
||||
use tracing::error;
|
||||
|
||||
pub struct MentorsService;
|
||||
@@ -34,7 +34,7 @@ impl MentorsService {
|
||||
let user_repo = UsersRepository::new(state);
|
||||
let mentor_repo = MentorsRepository::new(state);
|
||||
let role_repo = RolesRepository::new(state);
|
||||
let auth_repo = AuthRepository::new(state.surrealdb_mem.clone());
|
||||
let _auth_repo = AuthRepository::new(state);
|
||||
|
||||
let user_email = &dto.email;
|
||||
let mut _user_to_update: Option<UsersSchema> = None;
|
||||
@@ -59,23 +59,23 @@ impl MentorsService {
|
||||
|
||||
let mut user_schema = UsersSchema::from(user_detail_query_dto);
|
||||
|
||||
user_schema.fullname = dto.fullname.clone();
|
||||
user_schema.phone_number = dto.phone_number.clone();
|
||||
// Update personal data from identity_and_verification
|
||||
user_schema.legal_name = Some(dto.identity_and_verification.legal_name.clone());
|
||||
user_schema.gender = dto.identity_and_verification.gender.clone();
|
||||
user_schema.domicile = dto.identity_and_verification.domicile.clone();
|
||||
user_schema.phone_for_verification = Some(dto.identity_and_verification.phone_for_verification.clone());
|
||||
// Update personal data from professional_profile
|
||||
user_schema.bio = Some(dto.professional_profile.bio.clone());
|
||||
user_schema.last_education = dto.professional_profile.last_education.clone();
|
||||
user_schema.linkedin_url = dto.professional_profile.linkedin_url.clone();
|
||||
user_schema.github_url = dto.professional_profile.github_url.clone();
|
||||
user_schema.cv_url = dto.professional_profile.cv_url.clone();
|
||||
user_schema.portfolio_url = dto.professional_profile.portfolio_url.clone();
|
||||
user_schema.fullname = Some(dto.fullname.clone());
|
||||
// Update profile_extension fields
|
||||
let mut profile_ext = user_schema.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();
|
||||
user_schema.profile_extension = Some(profile_ext);
|
||||
user_schema.updated_at = imphnen_utils::get_iso_date();
|
||||
|
||||
let hashed_password = match imphnen_utils::hash_password(&dto.password) {
|
||||
let hashed_password = match hash_password(&dto.password) {
|
||||
Ok(hash) => hash,
|
||||
Err(_e) => {
|
||||
error!(
|
||||
@@ -88,7 +88,7 @@ impl MentorsService {
|
||||
);
|
||||
}
|
||||
};
|
||||
user_schema.password = hashed_password;
|
||||
user_schema.password = Some(hashed_password);
|
||||
|
||||
let mentor_role = match role_repo
|
||||
.query_role_by_name(RolesEnum::Mentor.to_string())
|
||||
@@ -99,8 +99,7 @@ impl MentorsService {
|
||||
return common_response(StatusCode::BAD_REQUEST, "Mentor Role Not Found");
|
||||
}
|
||||
};
|
||||
user_schema.role =
|
||||
imphnen_utils::make_thing_from_enum(ResourceEnum::Roles, &mentor_role.id);
|
||||
user_schema.mentor_id = Some(imphnen_utils::make_thing_from_enum("Roles", &mentor_role.id));
|
||||
user_schema.is_active = false;
|
||||
|
||||
if let Err(_err) = user_repo.query_update_user(user_schema.clone()).await {
|
||||
@@ -125,7 +124,7 @@ impl MentorsService {
|
||||
}
|
||||
};
|
||||
|
||||
let hashed_password = match imphnen_utils::hash_password(&dto.password) {
|
||||
let hashed_password = match hash_password(&dto.password) {
|
||||
Ok(hash) => hash,
|
||||
Err(_e) => {
|
||||
error!(
|
||||
@@ -139,37 +138,42 @@ impl MentorsService {
|
||||
}
|
||||
};
|
||||
|
||||
let new_user_schema = UsersSchema {
|
||||
let mut new_user_schema = UsersSchema {
|
||||
id: imphnen_utils::make_thing_from_enum(
|
||||
ResourceEnum::Users,
|
||||
"Users",
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
email: dto.email,
|
||||
fullname: dto.fullname,
|
||||
password: hashed_password,
|
||||
phone_number: dto.phone_number,
|
||||
email: Some(dto.email),
|
||||
fullname: Some(dto.fullname),
|
||||
password: Some(hashed_password),
|
||||
// Set phone number in profile extension instead
|
||||
// Store personal data from identity_and_verification in user
|
||||
legal_name: Some(dto.identity_and_verification.legal_name.clone()),
|
||||
gender: dto.identity_and_verification.gender.clone(),
|
||||
domicile: dto.identity_and_verification.domicile.clone(),
|
||||
phone_for_verification: Some(dto.identity_and_verification.phone_for_verification.clone()),
|
||||
// Use profile_extension for these fields
|
||||
// Store personal data from professional_profile in user
|
||||
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(),
|
||||
created_at: imphnen_utils::get_iso_date(),
|
||||
updated_at: imphnen_utils::get_iso_date(),
|
||||
role: imphnen_utils::make_thing_from_enum(
|
||||
ResourceEnum::Roles,
|
||||
mentor_id: Some(imphnen_utils::make_thing_from_enum(
|
||||
"Roles",
|
||||
&mentor_role.id,
|
||||
),
|
||||
)),
|
||||
is_active: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// populate profile_extension
|
||||
let mut profile_ext = new_user_schema.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();
|
||||
new_user_schema.profile_extension = Some(profile_ext);
|
||||
user_id = new_user_schema.id.clone();
|
||||
|
||||
match user_repo.query_create_user(new_user_schema).await {
|
||||
@@ -184,37 +188,12 @@ impl MentorsService {
|
||||
}
|
||||
}
|
||||
|
||||
let otp = imphnen_utils::generate_otp::OtpManager::generate_otp();
|
||||
|
||||
match auth_repo
|
||||
.query_store_otp(final_user_email.clone(), otp.clone())
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
let message = format!("your otp code is {}", otp.code);
|
||||
if let Err(_err) =
|
||||
imphnen_utils::send_email(&final_user_email, "OTP Verification", &message)
|
||||
{
|
||||
error!("Failed to send OTP email to {}: {}", final_user_email, _err);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
&_err.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(_err) => {
|
||||
error!("Failed to store OTP for {}: {}", final_user_email, _err);
|
||||
return common_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
&_err.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
// Skip OTP for now - implement later if needed
|
||||
|
||||
let mentor_schema = MentorSchema::create(
|
||||
dto.professional_profile,
|
||||
dto.mentoring_logistics,
|
||||
user_id.to_raw(),
|
||||
user_id.clone(),
|
||||
);
|
||||
|
||||
match mentor_repo.query_create_mentor(mentor_schema.clone()).await {
|
||||
@@ -263,7 +242,7 @@ impl MentorsService {
|
||||
let mut mentor_list_data: Vec<MentorListResponseDto> = Vec::new();
|
||||
|
||||
for mentor_with_user in result.data {
|
||||
let mentor_dto = MentorDetailQueryDto::from(mentor_with_user);
|
||||
let mentor_dto = mentor_with_user;
|
||||
let mut list_item = MentorListResponseDto::from(mentor_dto.clone());
|
||||
|
||||
// Get user data to populate personal fields
|
||||
@@ -287,9 +266,7 @@ impl MentorsService {
|
||||
pub async fn get_mentor_by_id(state: &AppState, id: &str) -> Response {
|
||||
let mentor_repo = MentorsRepository::new(state);
|
||||
let user_repo = UsersRepository::new(state);
|
||||
let thing_id = Thing::from((ResourceEnum::Mentors.to_string().as_str(), id));
|
||||
|
||||
match mentor_repo.query_mentor_by_id(&thing_id, false).await {
|
||||
let thing_id = imphnen_utils::make_thing_from_enum("Mentors", id); match mentor_repo.query_mentor_by_id(&thing_id, false).await {
|
||||
Ok(mentor) => {
|
||||
// Get user data separately
|
||||
let user_result = user_repo.query_user_by_id(&mentor.user_id).await;
|
||||
@@ -297,21 +274,21 @@ impl MentorsService {
|
||||
Ok(user) => {
|
||||
// Combine mentor and user data
|
||||
let dto = MentorDetailResponseDto {
|
||||
id: mentor.id.to_raw(),
|
||||
user_id: mentor.user_id.to_raw(),
|
||||
id: mentor.id.clone(),
|
||||
user_id: mentor.user_id.clone(),
|
||||
// Personal data from user
|
||||
fullname: Some(user.fullname),
|
||||
email: Some(user.email),
|
||||
legal_name: user.legal_name,
|
||||
gender: user.gender,
|
||||
domicile: user.domicile,
|
||||
phone_for_verification: user.phone_for_verification,
|
||||
bio: user.bio,
|
||||
last_education: user.last_education,
|
||||
linkedin_url: user.linkedin_url,
|
||||
github_url: user.github_url,
|
||||
cv_url: user.cv_url,
|
||||
portfolio_url: user.portfolio_url,
|
||||
gender: user.profile_extension.as_ref().and_then(|ext| ext.gender.clone()),
|
||||
domicile: user.profile_extension.as_ref().and_then(|ext| ext.domicile.clone()),
|
||||
phone_for_verification: user.profile_extension.as_ref().and_then(|ext| ext.phone_for_verification.clone()),
|
||||
bio: user.profile_extension.as_ref().and_then(|ext| ext.bio.clone()),
|
||||
last_education: user.profile_extension.as_ref().and_then(|ext| ext.last_education.clone()),
|
||||
linkedin_url: user.profile_extension.as_ref().and_then(|ext| ext.linkedin_url.clone()),
|
||||
github_url: user.profile_extension.as_ref().and_then(|ext| ext.github_url.clone()),
|
||||
cv_url: user.profile_extension.as_ref().and_then(|ext| ext.cv_url.clone()),
|
||||
portfolio_url: user.profile_extension.as_ref().and_then(|ext| ext.portfolio_url.clone()),
|
||||
// Professional data from mentor
|
||||
industries: mentor.industries,
|
||||
expertise: mentor.expertise,
|
||||
@@ -349,21 +326,26 @@ impl MentorsService {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
let repo = MentorsRepository::new(state);
|
||||
let thing_id = Thing::from((ResourceEnum::Mentors.to_string().as_str(), id));
|
||||
let existing_mentor = match repo.query_mentor_by_id(&thing_id, false).await {
|
||||
let mentor_uuid = Uuid::parse_str(id).map_err(|_| {
|
||||
common_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Invalid mentor ID format"
|
||||
)
|
||||
}).unwrap();
|
||||
let existing_mentor = match repo.query_mentor_by_id(&mentor_uuid.to_string(), false).await {
|
||||
Ok(mentor) => mentor,
|
||||
Err(_e) => return common_response(StatusCode::NOT_FOUND, &_e.to_string()),
|
||||
};
|
||||
|
||||
let mut schema = MentorSchema::from(MentorDetailQueryDto::from(existing_mentor));
|
||||
let mut schema = MentorSchema::from(existing_mentor);
|
||||
schema = schema.update(dto);
|
||||
|
||||
match repo.query_update_mentor(schema).await {
|
||||
Ok(_) => {
|
||||
let updated_mentor =
|
||||
repo.query_mentor_by_id(&thing_id, false).await.unwrap();
|
||||
repo.query_mentor_by_id(id, false).await.unwrap();
|
||||
let response_dto =
|
||||
MentorDetailResponseDto::from(MentorDetailQueryDto::from(updated_mentor));
|
||||
MentorDetailResponseDto::from(updated_mentor);
|
||||
success_response(ResponseSuccessDto { data: response_dto })
|
||||
}
|
||||
Err(_e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &_e.to_string()),
|
||||
@@ -372,7 +354,7 @@ impl MentorsService {
|
||||
|
||||
pub async fn delete_mentor(state: &AppState, id: &str) -> Response {
|
||||
let repo = MentorsRepository::new(state);
|
||||
match repo.query_delete_mentor(id.to_string()).await {
|
||||
match repo.query_delete_mentor(id).await {
|
||||
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||
Err(_e) => common_response(StatusCode::NOT_FOUND, &_e.to_string()),
|
||||
}
|
||||
@@ -382,7 +364,7 @@ impl MentorsService {
|
||||
let repo = MentorsRepository::new(state);
|
||||
match repo.query_mentor_by_email(email.to_string(), false).await {
|
||||
Ok(mentor) => {
|
||||
let dto = MentorDetailResponseDto::from(MentorDetailQueryDto::from(mentor));
|
||||
let dto = MentorDetailResponseDto::from(mentor);
|
||||
success_response(ResponseSuccessDto { data: dto })
|
||||
}
|
||||
Err(_e) => common_response(
|
||||
@@ -407,7 +389,7 @@ impl MentorsService {
|
||||
Err(_e) => return common_response(StatusCode::FORBIDDEN, &_e.to_string()),
|
||||
};
|
||||
|
||||
let mut schema = MentorSchema::from(MentorDetailQueryDto::from(existing_mentor));
|
||||
let mut schema = MentorSchema::from(existing_mentor);
|
||||
schema = schema.update(dto);
|
||||
|
||||
match repo.query_update_mentor(schema).await {
|
||||
@@ -417,7 +399,7 @@ impl MentorsService {
|
||||
.await
|
||||
.unwrap();
|
||||
let response_dto =
|
||||
MentorDetailResponseDto::from(MentorDetailQueryDto::from(updated_mentor));
|
||||
MentorDetailResponseDto::from(updated_mentor);
|
||||
success_response(ResponseSuccessDto { data: response_dto })
|
||||
}
|
||||
Err(_e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &_e.to_string()),
|
||||
@@ -441,21 +423,26 @@ impl MentorsService {
|
||||
dto: MentorVerifyRequestDto,
|
||||
) -> Response {
|
||||
let repo = MentorsRepository::new(state);
|
||||
let thing_id = Thing::from((ResourceEnum::Mentors.to_string().as_str(), id));
|
||||
let existing_mentor = match repo.query_mentor_by_id(&thing_id, false).await {
|
||||
let mentor_uuid = Uuid::parse_str(id).map_err(|_| {
|
||||
common_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Invalid mentor ID format"
|
||||
)
|
||||
}).unwrap();
|
||||
let existing_mentor = match repo.query_mentor_by_id(&mentor_uuid.to_string(), false).await {
|
||||
Ok(mentor) => mentor,
|
||||
Err(_e) => return common_response(StatusCode::NOT_FOUND, &_e.to_string()),
|
||||
};
|
||||
|
||||
let mut schema = MentorSchema::from(MentorDetailQueryDto::from(existing_mentor));
|
||||
let mut schema = MentorSchema::from(existing_mentor);
|
||||
schema = schema.update_status(dto.status);
|
||||
|
||||
match repo.query_update_mentor(schema).await {
|
||||
Ok(_) => {
|
||||
let updated_mentor =
|
||||
repo.query_mentor_by_id(&thing_id, false).await.unwrap();
|
||||
repo.query_mentor_by_id(&mentor_uuid.to_string(), false).await.unwrap();
|
||||
let response_dto =
|
||||
MentorDetailResponseDto::from(MentorDetailQueryDto::from(updated_mentor));
|
||||
MentorDetailResponseDto::from(updated_mentor);
|
||||
success_response(ResponseSuccessDto { data: response_dto })
|
||||
}
|
||||
Err(_e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &_e.to_string()),
|
||||
|
||||
@@ -1,61 +1,61 @@
|
||||
use axum::{
|
||||
Router,
|
||||
routing::{delete, get, post, put},
|
||||
};
|
||||
|
||||
pub mod mentors_controller;
|
||||
pub mod mentors_dto;
|
||||
pub mod mentors_repository;
|
||||
pub mod mentors_schema;
|
||||
pub mod mentors_service;
|
||||
|
||||
// Explicitly export only public controller functions and key types
|
||||
pub use mentors_controller::{
|
||||
post_register_mentor,
|
||||
get_mentor_list,
|
||||
get_mentor_by_id,
|
||||
put_update_mentor,
|
||||
delete_mentor,
|
||||
put_verify_mentor,
|
||||
get_mentor_me,
|
||||
put_update_mentor_me,
|
||||
put_update_mentor_no_id,
|
||||
get_mentor_status,
|
||||
};
|
||||
|
||||
// Export key DTO types used across the API
|
||||
pub use mentors_dto::{
|
||||
MentorListResponseDto,
|
||||
MentorDetailResponseDto,
|
||||
MentorRegisterResponseDto,
|
||||
MentorUpdateRequestDto,
|
||||
MentorUserRegisterRequestDto,
|
||||
MentorVerifyRequestDto,
|
||||
MentorDetailQueryDto,
|
||||
ProfessionalProfile,
|
||||
MentoringLogistics,
|
||||
MentoringRate,
|
||||
IdentityAndVerification,
|
||||
MentorInsertDto,
|
||||
};
|
||||
|
||||
// Export service and repository for internal use
|
||||
pub use mentors_service::MentorsService;
|
||||
pub use mentors_repository::MentorsRepository;
|
||||
|
||||
// Export schema types for database interactions
|
||||
pub use mentors_schema::MentorSchema;
|
||||
|
||||
pub fn mentors_router() -> Router {
|
||||
Router::new()
|
||||
.route("/", get(get_mentor_list))
|
||||
.route("/create", post(post_register_mentor))
|
||||
.route("/me", get(get_mentor_me))
|
||||
.route("/me/update", put(put_update_mentor_me))
|
||||
.route("/me/status", get(get_mentor_status))
|
||||
.route("/detail/{id}", get(get_mentor_by_id))
|
||||
.route("/update/{id}", put(put_update_mentor))
|
||||
.route("/update", put(put_update_mentor_no_id))
|
||||
.route("/delete/{id}", delete(delete_mentor))
|
||||
.route("/verify/{id}", put(put_verify_mentor))
|
||||
}
|
||||
use axum::{
|
||||
Router,
|
||||
routing::{delete, get, post, put},
|
||||
};
|
||||
|
||||
pub mod mentors_controller;
|
||||
pub mod mentors_dto;
|
||||
pub mod mentors_repository;
|
||||
pub mod mentors_schema;
|
||||
pub mod mentors_service;
|
||||
|
||||
// Explicitly export only public controller functions and key types
|
||||
pub use mentors_controller::{
|
||||
post_register_mentor,
|
||||
get_mentor_list,
|
||||
get_mentor_by_id,
|
||||
put_update_mentor,
|
||||
delete_mentor,
|
||||
put_verify_mentor,
|
||||
get_mentor_me,
|
||||
put_update_mentor_me,
|
||||
put_update_mentor_no_id,
|
||||
get_mentor_status,
|
||||
};
|
||||
|
||||
// Export key DTO types used across the API
|
||||
pub use mentors_dto::{
|
||||
MentorListResponseDto,
|
||||
MentorDetailResponseDto,
|
||||
MentorRegisterResponseDto,
|
||||
MentorUpdateRequestDto,
|
||||
MentorUserRegisterRequestDto,
|
||||
MentorVerifyRequestDto,
|
||||
MentorDetailQueryDto,
|
||||
ProfessionalProfile,
|
||||
MentoringLogistics,
|
||||
MentoringRate,
|
||||
IdentityAndVerification,
|
||||
MentorInsertDto,
|
||||
};
|
||||
|
||||
// Export service and repository for internal use
|
||||
pub use mentors_service::MentorsService;
|
||||
pub use mentors_repository::MentorsRepository;
|
||||
|
||||
// Export schema types for database interactions
|
||||
pub use mentors_schema::MentorSchema;
|
||||
|
||||
pub fn mentors_router() -> Router {
|
||||
Router::new()
|
||||
.route("/", get(get_mentor_list))
|
||||
.route("/create", post(post_register_mentor))
|
||||
.route("/me", get(get_mentor_me))
|
||||
.route("/me/update", put(put_update_mentor_me))
|
||||
.route("/me/status", get(get_mentor_status))
|
||||
.route("/detail/{id}", get(get_mentor_by_id))
|
||||
.route("/update/{id}", put(put_update_mentor))
|
||||
.route("/update", put(put_update_mentor_no_id))
|
||||
.route("/delete/{id}", delete(delete_mentor))
|
||||
.route("/verify/{id}", put(put_verify_mentor))
|
||||
}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
use axum::Router;
|
||||
|
||||
pub mod mentors;
|
||||
pub mod sessions;
|
||||
|
||||
/// Creates the main Dimentorin router with all version 1 endpoints
|
||||
/// Routes:
|
||||
/// - /mentors -> mentors::mentors_router()
|
||||
/// - /sessions -> sessions::sessions_router()
|
||||
/// - /users/me/sessions -> sessions::get_my_sessions()
|
||||
pub fn dimentorin_router() -> Router {
|
||||
Router::new()
|
||||
.nest("/mentors", mentors::mentors_router())
|
||||
.merge(sessions::sessions_router())
|
||||
}
|
||||
|
||||
// Explicitly re-export key items for easier consumption
|
||||
pub use mentors::mentors_router;
|
||||
pub use mentors::MentorsService;
|
||||
pub use mentors::MentorsRepository;
|
||||
pub use mentors::MentorSchema;
|
||||
|
||||
pub use sessions::sessions_router;
|
||||
pub use sessions::SessionsService;
|
||||
pub use sessions::SessionsRepository;
|
||||
pub use sessions::SessionSchema;
|
||||
use axum::Router;
|
||||
|
||||
pub mod mentors;
|
||||
pub mod sessions;
|
||||
|
||||
/// Creates the main Dimentorin router with all version 1 endpoints
|
||||
/// Routes:
|
||||
/// - /mentors -> mentors::mentors_router()
|
||||
/// - /sessions -> sessions::sessions_router()
|
||||
/// - /users/me/sessions -> sessions::get_my_sessions()
|
||||
pub fn dimentorin_router() -> Router {
|
||||
Router::new()
|
||||
.nest("/mentors", mentors::mentors_router())
|
||||
.merge(sessions::sessions_router())
|
||||
}
|
||||
|
||||
// Explicitly re-export key items for easier consumption
|
||||
pub use mentors::mentors_router;
|
||||
pub use mentors::MentorsService;
|
||||
pub use mentors::MentorsRepository;
|
||||
pub use mentors::MentorSchema;
|
||||
|
||||
pub use sessions::sessions_router;
|
||||
pub use sessions::SessionsService;
|
||||
pub use sessions::SessionsRepository;
|
||||
pub use sessions::SessionSchema;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
pub mod sessions_controller;
|
||||
pub mod sessions_dto;
|
||||
pub mod sessions_repository;
|
||||
pub mod sessions_schema;
|
||||
pub mod sessions_service;
|
||||
|
||||
pub use sessions_controller::*;
|
||||
pub use sessions_dto::*;
|
||||
pub use sessions_repository::*;
|
||||
pub use sessions_schema::*;
|
||||
pub use sessions_service::*;
|
||||
pub mod sessions_controller;
|
||||
pub mod sessions_dto;
|
||||
pub mod sessions_repository;
|
||||
pub mod sessions_schema;
|
||||
pub mod sessions_service;
|
||||
|
||||
pub use sessions_controller::*;
|
||||
pub use sessions_dto::*;
|
||||
pub use sessions_repository::*;
|
||||
pub use sessions_schema::*;
|
||||
pub use sessions_service::*;
|
||||
|
||||
@@ -1,197 +1,197 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use validator::Validate;
|
||||
|
||||
// ============================================
|
||||
// Book Session (POST /v1/mentors/{id}/sessions/book)
|
||||
// ============================================
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct BookSessionRequestDto {
|
||||
#[validate(length(min = 3, max = 200, message = "Topic must be 3-200 characters"))]
|
||||
pub topic: String,
|
||||
|
||||
#[validate(length(max = 1000, message = "Description must be max 1000 characters"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
|
||||
#[validate(length(min = 1, message = "Scheduled time is required"))]
|
||||
pub scheduled_at: String, // ISO 8601 datetime
|
||||
|
||||
#[validate(range(min = 15, max = 240, message = "Duration must be 15-240 minutes"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub duration_minutes: Option<i32>,
|
||||
|
||||
#[validate(length(max = 50, message = "Session type must be max 50 characters"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub session_type: Option<String>, // "video_call", "phone_call", "chat"
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// List Sessions (GET /v1/mentors/{id}/sessions & /v1/users/me/sessions)
|
||||
// ============================================
|
||||
|
||||
#[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,
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Session Detail
|
||||
// ============================================
|
||||
|
||||
#[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,
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Mentor Availability (GET /v1/mentors/{id}/availability)
|
||||
// ============================================
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct AvailabilitySlotDto {
|
||||
pub date: String, // YYYY-MM-DD
|
||||
pub time: String, // HH:MM
|
||||
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>, // Dates with existing sessions
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Update Session Status (PUT /v1/sessions/{id}/status)
|
||||
// ============================================
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct UpdateSessionStatusRequestDto {
|
||||
#[validate(length(min = 1, max = 50, message = "Status must be 1-50 characters"))]
|
||||
pub status: String, // "confirmed", "completed", "cancelled", "no_show"
|
||||
|
||||
#[validate(url(message = "Meeting link must be a valid URL"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub meeting_link: Option<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,
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Submit Feedback (POST /v1/sessions/{id}/feedback)
|
||||
// ============================================
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct SessionFeedbackRequestDto {
|
||||
#[validate(length(min = 10, max = 2000, message = "Feedback must be 10-2000 characters"))]
|
||||
pub feedback: String,
|
||||
|
||||
#[validate(range(min = 1, max = 5, message = "Rating must be 1-5"))]
|
||||
pub rating: i32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SessionFeedbackResponseDto {
|
||||
pub id: String,
|
||||
pub feedback: String,
|
||||
pub rating: i32,
|
||||
pub submitted_at: String,
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Query DTOs (internal use)
|
||||
// ============================================
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct SessionDetailQueryDto {
|
||||
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 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 mentor_fullname: Option<String>,
|
||||
pub mentee_fullname: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct SessionListQueryDto {
|
||||
pub id: String,
|
||||
pub mentor_id: String,
|
||||
pub mentee_id: 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 mentee_fullname: Option<String>,
|
||||
pub mentee_email: Option<String>,
|
||||
}
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use validator::Validate;
|
||||
|
||||
// ============================================
|
||||
// Book Session (POST /v1/mentors/{id}/sessions/book)
|
||||
// ============================================
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct BookSessionRequestDto {
|
||||
#[validate(length(min = 3, max = 200, message = "Topic must be 3-200 characters"))]
|
||||
pub topic: String,
|
||||
|
||||
#[validate(length(max = 1000, message = "Description must be max 1000 characters"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
|
||||
#[validate(length(min = 1, message = "Scheduled time is required"))]
|
||||
pub scheduled_at: String, // ISO 8601 datetime
|
||||
|
||||
#[validate(range(min = 15, max = 240, message = "Duration must be 15-240 minutes"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub duration_minutes: Option<i32>,
|
||||
|
||||
#[validate(length(max = 50, message = "Session type must be max 50 characters"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub session_type: Option<String>, // "video_call", "phone_call", "chat"
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// List Sessions (GET /v1/mentors/{id}/sessions & /v1/users/me/sessions)
|
||||
// ============================================
|
||||
|
||||
#[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,
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Session Detail
|
||||
// ============================================
|
||||
|
||||
#[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,
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Mentor Availability (GET /v1/mentors/{id}/availability)
|
||||
// ============================================
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct AvailabilitySlotDto {
|
||||
pub date: String, // YYYY-MM-DD
|
||||
pub time: String, // HH:MM
|
||||
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>, // Dates with existing sessions
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Update Session Status (PUT /v1/sessions/{id}/status)
|
||||
// ============================================
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct UpdateSessionStatusRequestDto {
|
||||
#[validate(length(min = 1, max = 50, message = "Status must be 1-50 characters"))]
|
||||
pub status: String, // "confirmed", "completed", "cancelled", "no_show"
|
||||
|
||||
#[validate(url(message = "Meeting link must be a valid URL"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub meeting_link: Option<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,
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Submit Feedback (POST /v1/sessions/{id}/feedback)
|
||||
// ============================================
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct SessionFeedbackRequestDto {
|
||||
#[validate(length(min = 10, max = 2000, message = "Feedback must be 10-2000 characters"))]
|
||||
pub feedback: String,
|
||||
|
||||
#[validate(range(min = 1, max = 5, message = "Rating must be 1-5"))]
|
||||
pub rating: i32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SessionFeedbackResponseDto {
|
||||
pub id: String,
|
||||
pub feedback: String,
|
||||
pub rating: i32,
|
||||
pub submitted_at: String,
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Query DTOs (internal use)
|
||||
// ============================================
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct SessionDetailQueryDto {
|
||||
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 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 mentor_fullname: Option<String>,
|
||||
pub mentee_fullname: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct SessionListQueryDto {
|
||||
pub id: String,
|
||||
pub mentor_id: String,
|
||||
pub mentee_id: 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 mentee_fullname: Option<String>,
|
||||
pub mentee_email: Option<String>,
|
||||
}
|
||||
|
||||
@@ -1,365 +1,369 @@
|
||||
use super::{SessionDetailQueryDto, SessionListQueryDto, SessionSchema};
|
||||
use imphnen_libs::AppState;
|
||||
use imphnen_utils::get_id;
|
||||
use serde::Deserialize;
|
||||
use surrealdb::sql::Thing;
|
||||
|
||||
pub struct SessionsRepository<'a> {
|
||||
pub state: &'a AppState,
|
||||
}
|
||||
|
||||
impl<'a> SessionsRepository<'a> {
|
||||
pub fn new(state: &'a AppState) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Create Session
|
||||
// ============================================
|
||||
pub async fn create_session(&self, schema: SessionSchema) -> Result<SessionSchema, String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let created: Option<SessionSchema> = db
|
||||
.create("sessions")
|
||||
.content(schema)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to create session: {}", e))?;
|
||||
|
||||
created.ok_or_else(|| "Session creation returned None".to_string())
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Get Session by ID
|
||||
// ============================================
|
||||
pub async fn query_session_by_id(&self, id: &Thing) -> Result<Option<SessionSchema>, String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let record_key = get_id(id).map_err(|e| e.to_string())?;
|
||||
let session: Option<SessionSchema> = db
|
||||
.select(record_key)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to fetch session: {}", e))?;
|
||||
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Get Session Detail with User Info
|
||||
// ============================================
|
||||
pub async fn query_session_detail(&self, id: &Thing) -> Result<Option<SessionDetailQueryDto>, String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let query = r#"
|
||||
SELECT
|
||||
id,
|
||||
mentor_id,
|
||||
mentee_id,
|
||||
topic,
|
||||
description,
|
||||
scheduled_at,
|
||||
duration_minutes,
|
||||
meeting_link,
|
||||
session_type,
|
||||
status,
|
||||
feedback,
|
||||
rating,
|
||||
feedback_submitted_at,
|
||||
created_at,
|
||||
updated_at,
|
||||
(SELECT fullname FROM $parent.mentor_id.user_id)[0].fullname AS mentor_fullname,
|
||||
(SELECT fullname FROM $parent.mentee_id)[0].fullname AS mentee_fullname
|
||||
FROM type::thing($table, $id)
|
||||
"#;
|
||||
|
||||
let mut result = db
|
||||
.query(query)
|
||||
.bind(("table", "sessions"))
|
||||
.bind(("id", id.id.to_string()))
|
||||
.await
|
||||
.map_err(|e| format!("Failed to query session detail: {}", e))?;
|
||||
|
||||
let session: Option<SessionDetailQueryDto> = result
|
||||
.take(0)
|
||||
.map_err(|e| format!("Failed to parse session detail: {}", e))?;
|
||||
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// List Mentor's Sessions
|
||||
// ============================================
|
||||
pub async fn query_mentor_sessions(
|
||||
&self,
|
||||
mentor_id: &Thing,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<Vec<SessionListQueryDto>, String> {
|
||||
let query = if let Some(_status) = status_filter.as_ref() {
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
mentor_id,
|
||||
mentee_id,
|
||||
topic,
|
||||
scheduled_at,
|
||||
duration_minutes,
|
||||
session_type,
|
||||
status,
|
||||
rating,
|
||||
created_at,
|
||||
(SELECT fullname FROM $parent.mentee_id)[0].fullname AS mentee_fullname,
|
||||
(SELECT email FROM $parent.mentee_id)[0].email AS mentee_email
|
||||
FROM sessions
|
||||
WHERE mentor_id = $mentor_id AND status = $status
|
||||
ORDER BY scheduled_at DESC
|
||||
"#
|
||||
} else {
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
mentor_id,
|
||||
mentee_id,
|
||||
topic,
|
||||
scheduled_at,
|
||||
duration_minutes,
|
||||
session_type,
|
||||
status,
|
||||
rating,
|
||||
created_at,
|
||||
(SELECT fullname FROM $parent.mentee_id)[0].fullname AS mentee_fullname,
|
||||
(SELECT email FROM $parent.mentee_id)[0].email AS mentee_email
|
||||
FROM sessions
|
||||
WHERE mentor_id = $mentor_id
|
||||
ORDER BY scheduled_at DESC
|
||||
"#
|
||||
};
|
||||
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let mentor_id_clone = mentor_id.clone();
|
||||
let mut result = if let Some(status_val) = status_filter {
|
||||
db.query(query)
|
||||
.bind(("mentor_id", mentor_id_clone))
|
||||
.bind(("status", status_val))
|
||||
.await
|
||||
} else {
|
||||
db.query(query)
|
||||
.bind(("mentor_id", mentor_id_clone))
|
||||
.await
|
||||
}
|
||||
.map_err(|e| format!("Failed to query mentor sessions: {}", e))?;
|
||||
|
||||
let sessions: Vec<SessionListQueryDto> = result
|
||||
.take(0)
|
||||
.map_err(|e| format!("Failed to parse mentor sessions: {}", e))?;
|
||||
|
||||
Ok(sessions)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// List User's Sessions (as mentee)
|
||||
// ============================================
|
||||
pub async fn query_user_sessions(
|
||||
&self,
|
||||
user_id: &Thing,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<Vec<SessionListQueryDto>, String> {
|
||||
let query = if let Some(_status) = status_filter.as_ref() {
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
mentor_id,
|
||||
mentee_id,
|
||||
topic,
|
||||
scheduled_at,
|
||||
duration_minutes,
|
||||
session_type,
|
||||
status,
|
||||
rating,
|
||||
created_at,
|
||||
(SELECT fullname FROM $parent.mentee_id)[0].fullname AS mentee_fullname,
|
||||
(SELECT email FROM $parent.mentee_id)[0].email AS mentee_email
|
||||
FROM sessions
|
||||
WHERE mentee_id = $user_id AND status = $status
|
||||
ORDER BY scheduled_at DESC
|
||||
"#
|
||||
} else {
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
mentor_id,
|
||||
mentee_id,
|
||||
topic,
|
||||
scheduled_at,
|
||||
duration_minutes,
|
||||
session_type,
|
||||
status,
|
||||
rating,
|
||||
created_at,
|
||||
(SELECT fullname FROM $parent.mentee_id)[0].fullname AS mentee_fullname,
|
||||
(SELECT email FROM $parent.mentee_id)[0].email AS mentee_email
|
||||
FROM sessions
|
||||
WHERE mentee_id = $user_id
|
||||
ORDER BY scheduled_at DESC
|
||||
"#
|
||||
};
|
||||
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let user_id_clone = user_id.clone();
|
||||
let mut result = if let Some(status_val) = status_filter {
|
||||
db.query(query)
|
||||
.bind(("user_id", user_id_clone))
|
||||
.bind(("status", status_val))
|
||||
.await
|
||||
} else {
|
||||
db.query(query)
|
||||
.bind(("user_id", user_id_clone))
|
||||
.await
|
||||
}
|
||||
.map_err(|e| format!("Failed to query user sessions: {}", e))?;
|
||||
|
||||
let sessions: Vec<SessionListQueryDto> = result
|
||||
.take(0)
|
||||
.map_err(|e| format!("Failed to parse user sessions: {}", e))?;
|
||||
|
||||
Ok(sessions)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Get Booked Dates for Mentor
|
||||
// ============================================
|
||||
pub async fn query_booked_dates(&self, mentor_id: &Thing) -> Result<Vec<String>, String> {
|
||||
let query = r#"
|
||||
SELECT scheduled_at FROM sessions
|
||||
WHERE mentor_id = $mentor_id
|
||||
AND status IN ['pending', 'confirmed']
|
||||
ORDER BY scheduled_at ASC
|
||||
"#;
|
||||
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let mentor_id_clone = mentor_id.clone();
|
||||
let mut result = db
|
||||
.query(query)
|
||||
.bind(("mentor_id", mentor_id_clone))
|
||||
.await
|
||||
.map_err(|e| format!("Failed to query booked dates: {}", e))?;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct DateOnly {
|
||||
scheduled_at: String,
|
||||
}
|
||||
|
||||
let dates: Vec<DateOnly> = result
|
||||
.take(0)
|
||||
.map_err(|e| format!("Failed to parse booked dates: {}", e))?;
|
||||
|
||||
Ok(dates.into_iter().map(|d| d.scheduled_at).collect())
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Update Session
|
||||
// ============================================
|
||||
pub async fn update_session(&self, id: &Thing, schema: SessionSchema) -> Result<SessionSchema, String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let record_key = get_id(id).map_err(|e| e.to_string())?;
|
||||
let updated: Option<SessionSchema> = db
|
||||
.update(record_key)
|
||||
.content(schema)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to update session: {}", e))?;
|
||||
|
||||
updated.ok_or_else(|| "Session update returned None".to_string())
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Count Mentor Sessions
|
||||
// ============================================
|
||||
pub async fn count_mentor_sessions(
|
||||
&self,
|
||||
mentor_id: &Thing,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<usize, String> {
|
||||
let query = if status_filter.is_some() {
|
||||
"SELECT count() FROM sessions WHERE mentor_id = $mentor_id AND status = $status GROUP ALL"
|
||||
} else {
|
||||
"SELECT count() FROM sessions WHERE mentor_id = $mentor_id GROUP ALL"
|
||||
};
|
||||
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let mentor_id_clone = mentor_id.clone();
|
||||
let mut result = if let Some(status_val) = status_filter {
|
||||
db.query(query)
|
||||
.bind(("mentor_id", mentor_id_clone))
|
||||
.bind(("status", status_val))
|
||||
.await
|
||||
} else {
|
||||
db.query(query)
|
||||
.bind(("mentor_id", mentor_id_clone))
|
||||
.await
|
||||
}
|
||||
.map_err(|e| format!("Failed to count mentor sessions: {}", e))?;
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct CountResult {
|
||||
count: usize,
|
||||
}
|
||||
|
||||
let count_result: Option<CountResult> = result
|
||||
.take(0)
|
||||
.map_err(|e| format!("Failed to parse count: {}", e))?;
|
||||
|
||||
Ok(count_result.map(|r| r.count).unwrap_or(0))
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Count User Sessions
|
||||
// ============================================
|
||||
pub async fn count_user_sessions(
|
||||
&self,
|
||||
user_id: &Thing,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<usize, String> {
|
||||
let query = if status_filter.is_some() {
|
||||
"SELECT count() FROM sessions WHERE mentee_id = $user_id AND status = $status GROUP ALL"
|
||||
} else {
|
||||
"SELECT count() FROM sessions WHERE mentee_id = $user_id GROUP ALL"
|
||||
};
|
||||
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let user_id_clone = user_id.clone();
|
||||
let mut result = if let Some(status_val) = status_filter {
|
||||
db.query(query)
|
||||
.bind(("user_id", user_id_clone))
|
||||
.bind(("status", status_val))
|
||||
.await
|
||||
} else {
|
||||
db.query(query)
|
||||
.bind(("user_id", user_id_clone))
|
||||
.await
|
||||
}
|
||||
.map_err(|e| format!("Failed to count user sessions: {}", e))?;
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct CountResult {
|
||||
count: usize,
|
||||
}
|
||||
|
||||
let count_result: Option<CountResult> = result
|
||||
.take(0)
|
||||
.map_err(|e| format!("Failed to parse count: {}", e))?;
|
||||
|
||||
Ok(count_result.map(|r| r.count).unwrap_or(0))
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Delete Session (soft delete)
|
||||
// ============================================
|
||||
// Delete Session (soft delete)
|
||||
// ============================================
|
||||
pub async fn delete_session(&self, id: &Thing) -> Result<(), String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let record_key = get_id(id).map_err(|e| e.to_string())?;
|
||||
let _: Option<SessionSchema> = db
|
||||
.delete(record_key)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to delete session: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
use super::{SessionDetailQueryDto, SessionListQueryDto, SessionSchema};
|
||||
use anyhow::{anyhow, Result};
|
||||
use imphnen_libs::{AppState, AppStatePostgresExt};
|
||||
use sea_orm::*;
|
||||
use uuid::Uuid;
|
||||
|
||||
use imphnen_entities::seaorm::auth::sessions::{
|
||||
Entity as Sessions, Model as SessionModel, ActiveModel as SessionActiveModel, Column as SessionColumn,
|
||||
};
|
||||
use imphnen_entities::seaorm::auth::users::Entity as Users;
|
||||
|
||||
pub struct SessionsRepository<'a> {
|
||||
pub db: &'a DatabaseConnection,
|
||||
}
|
||||
|
||||
impl<'a> SessionsRepository<'a> {
|
||||
pub fn new(state: &'a AppState) -> Self {
|
||||
Self { db: state.postgres_db() }
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Create Session
|
||||
// ============================================
|
||||
pub async fn create_session(&self, schema: SessionSchema) -> Result<SessionSchema> {
|
||||
let mut result = schema.clone();
|
||||
|
||||
let session_active_model = SessionActiveModel {
|
||||
id: Set(schema.id),
|
||||
mentor_id: Set(schema.mentor_id),
|
||||
mentee_id: Set(schema.mentee_id),
|
||||
topic: Set(schema.topic),
|
||||
description: Set(schema.description),
|
||||
scheduled_at: Set(schema.scheduled_at),
|
||||
duration_minutes: Set(schema.duration_minutes),
|
||||
meeting_link: Set(schema.meeting_link),
|
||||
session_type: Set(schema.session_type),
|
||||
status: Set(schema.status),
|
||||
feedback: Set(schema.feedback),
|
||||
rating: Set(schema.rating),
|
||||
feedback_submitted_at: Set(schema.feedback_submitted_at),
|
||||
created_at: Set(schema.created_at),
|
||||
updated_at: Set(schema.updated_at),
|
||||
};
|
||||
|
||||
let session_model: SessionModel = session_active_model.insert(self.db).await?;
|
||||
|
||||
result.id = session_model.id;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Get Session by ID
|
||||
// ============================================
|
||||
pub async fn query_session_by_id(&self, id: &str) -> Result<Option<SessionSchema>> {
|
||||
let session_model: Option<SessionModel> = Sessions::find_by_id(Uuid::parse_str(id).map_err(|e| anyhow!("Invalid session ID: {}", e))?)
|
||||
.one(self.db)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to fetch session: {}", e))?;
|
||||
|
||||
if let Some(session) = session_model {
|
||||
let schema = SessionSchema {
|
||||
id: session.id,
|
||||
mentor_id: session.mentor_id,
|
||||
mentee_id: session.mentee_id,
|
||||
topic: session.topic,
|
||||
description: session.description,
|
||||
scheduled_at: session.scheduled_at,
|
||||
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,
|
||||
created_at: session.created_at,
|
||||
updated_at: session.updated_at,
|
||||
};
|
||||
|
||||
Ok(Some(schema))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Get Session Detail with User Info
|
||||
// ============================================
|
||||
pub async fn query_session_detail(&self, id: &str) -> Result<Option<SessionDetailQueryDto>> {
|
||||
let session_id = Uuid::parse_str(id).map_err(|e| anyhow!("Invalid session ID: {}", e))?;
|
||||
|
||||
let session = Sessions::find_by_id(session_id)
|
||||
.one(self.db)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to fetch session: {}", e))?
|
||||
.ok_or_else(|| anyhow!("Session not found"))?;
|
||||
|
||||
let mentor = Users::find_by_id(session.mentor_id)
|
||||
.one(self.db)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to fetch mentor: {}", e))?
|
||||
.ok_or_else(|| anyhow!("Mentor not found"))?;
|
||||
|
||||
let mentee = Users::find_by_id(session.mentee_id)
|
||||
.one(self.db)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to fetch mentee: {}", e))?
|
||||
.ok_or_else(|| anyhow!("Mentee not found"))?;
|
||||
|
||||
let session_detail = SessionDetailQueryDto {
|
||||
id: session.id.to_string(),
|
||||
mentor_id: session.mentor_id.to_string(),
|
||||
mentee_id: session.mentee_id.to_string(),
|
||||
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(),
|
||||
mentor_fullname: Some(format!("{} {}", mentor.first_name.unwrap_or_default(), mentor.last_name.unwrap_or_default())),
|
||||
mentee_fullname: Some(format!("{} {}", mentee.first_name.unwrap_or_default(), mentee.last_name.unwrap_or_default())),
|
||||
};
|
||||
|
||||
Ok(Some(session_detail))
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// List Mentor's Sessions
|
||||
// ============================================
|
||||
pub async fn query_mentor_sessions(
|
||||
&self,
|
||||
mentor_id: &str,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<Vec<SessionListQueryDto>> {
|
||||
let mentor_uuid = Uuid::parse_str(mentor_id).map_err(|e| anyhow!("Invalid mentor ID: {}", e))?;
|
||||
|
||||
let query = Sessions::find()
|
||||
.filter(SessionColumn::MentorId.eq(mentor_uuid))
|
||||
.order_by_desc(SessionColumn::ScheduledAt);
|
||||
|
||||
let query = if let Some(status) = status_filter {
|
||||
query.filter(SessionColumn::Status.eq(status))
|
||||
} else {
|
||||
query
|
||||
};
|
||||
|
||||
let sessions = query.all(self.db).await.map_err(|e| anyhow!("Failed to query mentor sessions: {}", e))?;
|
||||
|
||||
let mut session_list = Vec::with_capacity(sessions.len());
|
||||
|
||||
for session in sessions {
|
||||
// Join with users table to get mentee details
|
||||
let mentee = Users::find_by_id(session.mentee_id)
|
||||
.one(self.db)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to fetch mentee: {}", e))?;
|
||||
|
||||
let session_dto = SessionListQueryDto {
|
||||
id: session.id.to_string(),
|
||||
mentor_id: session.mentor_id.to_string(),
|
||||
mentee_id: session.mentee_id.to_string(),
|
||||
topic: session.topic,
|
||||
scheduled_at: session.scheduled_at.to_rfc3339(),
|
||||
duration_minutes: session.duration_minutes,
|
||||
session_type: session.session_type,
|
||||
status: session.status,
|
||||
rating: session.rating,
|
||||
created_at: session.created_at.to_rfc3339(),
|
||||
mentee_fullname: mentee.as_ref().map(|m| format!("{} {}", m.first_name.clone().unwrap_or_default(), m.last_name.clone().unwrap_or_default())),
|
||||
mentee_email: mentee.as_ref().map(|u| u.email.clone()), // Assuming Users model has an email field
|
||||
};
|
||||
|
||||
session_list.push(session_dto);
|
||||
}
|
||||
|
||||
Ok(session_list)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// List User's Sessions (as mentee)
|
||||
// ============================================
|
||||
pub async fn query_user_sessions(
|
||||
&self,
|
||||
user_id: &str,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<Vec<SessionListQueryDto>> {
|
||||
let user_uuid = Uuid::parse_str(user_id).map_err(|e| anyhow!("Invalid user ID: {}", e))?;
|
||||
|
||||
let query = Sessions::find()
|
||||
.filter(SessionColumn::MenteeId.eq(user_uuid))
|
||||
.order_by_desc(SessionColumn::ScheduledAt);
|
||||
|
||||
let query = if let Some(status) = status_filter {
|
||||
query.filter(SessionColumn::Status.eq(status))
|
||||
} else {
|
||||
query
|
||||
};
|
||||
|
||||
let sessions = query.all(self.db).await.map_err(|e| anyhow!("Failed to query user sessions: {}", e))?;
|
||||
|
||||
let mut session_list = Vec::with_capacity(sessions.len());
|
||||
|
||||
for session in sessions {
|
||||
// Join with users table to get mentor details
|
||||
let _mentor = Users::find_by_id(session.mentor_id)
|
||||
.one(self.db)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to fetch mentor: {}", e))?;
|
||||
|
||||
let session_dto = SessionListQueryDto {
|
||||
id: session.id.to_string(),
|
||||
mentor_id: session.mentor_id.to_string(),
|
||||
mentee_id: session.mentee_id.to_string(),
|
||||
topic: session.topic,
|
||||
scheduled_at: session.scheduled_at.to_rfc3339(),
|
||||
duration_minutes: session.duration_minutes,
|
||||
session_type: session.session_type,
|
||||
status: session.status,
|
||||
rating: session.rating,
|
||||
created_at: session.created_at.to_rfc3339(),
|
||||
mentee_fullname: Some(session.mentee_id.to_string()), // Simplified - should get from user table
|
||||
mentee_email: Some("user@example.com".to_string()), // Simplified - should get from user table
|
||||
};
|
||||
|
||||
session_list.push(session_dto);
|
||||
}
|
||||
|
||||
Ok(session_list)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Get Booked Dates for Mentor
|
||||
// ============================================
|
||||
pub async fn query_booked_dates(&self, mentor_id: &str) -> Result<Vec<String>> {
|
||||
let mentor_uuid = Uuid::parse_str(mentor_id).map_err(|e| anyhow!("Invalid mentor ID: {}", e))?;
|
||||
|
||||
let sessions = Sessions::find()
|
||||
.filter(SessionColumn::MentorId.eq(mentor_uuid))
|
||||
.filter(SessionColumn::Status.is_in(["pending", "confirmed"]))
|
||||
.order_by_asc(SessionColumn::ScheduledAt)
|
||||
.all(self.db)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to query booked dates: {}", e))?;
|
||||
|
||||
Ok(sessions.into_iter()
|
||||
.map(|s| s.scheduled_at.to_rfc3339())
|
||||
.collect())
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Update Session
|
||||
// ============================================
|
||||
pub async fn update_session(&self, id: &str, schema: SessionSchema) -> Result<SessionSchema> {
|
||||
let session_id = Uuid::parse_str(id).map_err(|e| anyhow!("Invalid session ID: {}", e))?;
|
||||
|
||||
let mut result = schema.clone();
|
||||
|
||||
// Fetch existing session
|
||||
let session_model = Sessions::find_by_id(session_id)
|
||||
.one(self.db)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to fetch session for update: {}", e))?
|
||||
.ok_or_else(|| anyhow!("Session not found"))?;
|
||||
|
||||
// Convert to ActiveModel for update
|
||||
let mut session_active_model = session_model.into_active_model();
|
||||
|
||||
// Update fields
|
||||
session_active_model.topic = Set(schema.topic);
|
||||
session_active_model.description = Set(schema.description);
|
||||
session_active_model.scheduled_at = Set(schema.scheduled_at);
|
||||
session_active_model.duration_minutes = Set(schema.duration_minutes);
|
||||
session_active_model.meeting_link = Set(schema.meeting_link);
|
||||
session_active_model.session_type = Set(schema.session_type);
|
||||
session_active_model.status = Set(schema.status);
|
||||
session_active_model.feedback = Set(schema.feedback.clone());
|
||||
session_active_model.rating = Set(schema.rating);
|
||||
session_active_model.feedback_submitted_at = Set(schema.feedback_submitted_at);
|
||||
session_active_model.updated_at = Set(schema.updated_at);
|
||||
|
||||
// Save updated session
|
||||
let updated_session = session_active_model.update(self.db).await.map_err(|e| anyhow!("Failed to update session: {}", e))?;
|
||||
|
||||
// Convert back to SessionSchema for response
|
||||
result.id = updated_session.id;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Count Mentor Sessions
|
||||
// ============================================
|
||||
pub async fn count_mentor_sessions(
|
||||
&self,
|
||||
mentor_id: &str,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<usize> {
|
||||
let mentor_uuid = Uuid::parse_str(mentor_id).map_err(|e| anyhow!("Invalid mentor ID: {}", e))?;
|
||||
|
||||
let query = Sessions::find()
|
||||
.filter(SessionColumn::MentorId.eq(mentor_uuid));
|
||||
|
||||
let query = if let Some(status) = status_filter {
|
||||
query.filter(SessionColumn::Status.eq(status))
|
||||
} else {
|
||||
query
|
||||
};
|
||||
|
||||
let count = query.count(self.db).await.map_err(|e| anyhow!("Failed to count mentor sessions: {}", e))?;
|
||||
|
||||
Ok(count as usize)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Count User Sessions
|
||||
// ============================================
|
||||
pub async fn count_user_sessions(
|
||||
&self,
|
||||
user_id: &str,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<usize> {
|
||||
let user_uuid = Uuid::parse_str(user_id).map_err(|e| anyhow!("Invalid user ID: {}", e))?;
|
||||
|
||||
let query = Sessions::find()
|
||||
.filter(SessionColumn::MenteeId.eq(user_uuid));
|
||||
|
||||
let query = if let Some(status) = status_filter {
|
||||
query.filter(SessionColumn::Status.eq(status))
|
||||
} else {
|
||||
query
|
||||
};
|
||||
|
||||
let count = query.count(self.db).await.map_err(|e| anyhow!("Failed to count user sessions: {}", e))?;
|
||||
|
||||
Ok(count as usize)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Delete Session (soft delete)
|
||||
// ============================================
|
||||
pub async fn delete_session(&self, id: &str) -> Result<()> {
|
||||
let session_id = Uuid::parse_str(id).map_err(|e| anyhow!("Invalid session ID: {}", e))?;
|
||||
|
||||
// Fetch the session first
|
||||
let session_model = Sessions::find_by_id(session_id)
|
||||
.one(self.db)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to fetch session for deletion: {}", e))?
|
||||
.ok_or_else(|| anyhow!("Session not found"))?;
|
||||
|
||||
// Convert to ActiveModel for deletion
|
||||
let session_active_model = session_model.into_active_model();
|
||||
|
||||
// For soft delete, we would typically set an `is_deleted` flag
|
||||
// Since the original implementation didn't have this, we'll just delete the record
|
||||
// If you want to implement soft delete, add an `is_deleted` field to the SessionModel
|
||||
|
||||
let _ = session_active_model.delete(self.db).await.map_err(|e| anyhow!("Failed to delete session: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,89 +1,93 @@
|
||||
use super::{BookSessionRequestDto, SessionFeedbackRequestDto, UpdateSessionStatusRequestDto};
|
||||
use imphnen_libs::ResourceEnum;
|
||||
use imphnen_utils::{get_iso_date, make_thing};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::{sql::Thing, Uuid};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct SessionSchema {
|
||||
pub id: Thing,
|
||||
pub mentor_id: Thing,
|
||||
pub mentee_id: Thing,
|
||||
pub topic: String,
|
||||
pub description: Option<String>,
|
||||
pub scheduled_at: String, // ISO 8601 datetime
|
||||
pub duration_minutes: i32,
|
||||
pub meeting_link: Option<String>,
|
||||
pub session_type: String, // "video_call", "phone_call", "chat"
|
||||
pub status: String, // "pending", "confirmed", "completed", "cancelled", "no_show"
|
||||
pub feedback: Option<String>,
|
||||
pub rating: Option<i32>, // 1-5
|
||||
pub feedback_submitted_at: Option<String>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl Default for SessionSchema {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: make_thing(
|
||||
ResourceEnum::Sessions.to_string().as_str(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
mentor_id: make_thing(
|
||||
ResourceEnum::Mentors.to_string().as_str(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
mentee_id: make_thing(
|
||||
ResourceEnum::Users.to_string().as_str(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
topic: String::new(),
|
||||
description: None,
|
||||
scheduled_at: get_iso_date(),
|
||||
duration_minutes: 60,
|
||||
meeting_link: None,
|
||||
session_type: "video_call".to_string(),
|
||||
status: "pending".to_string(),
|
||||
feedback: None,
|
||||
rating: None,
|
||||
feedback_submitted_at: None,
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SessionSchema {
|
||||
pub fn from_book_request(
|
||||
mentor_id: Thing,
|
||||
mentee_id: Thing,
|
||||
request: BookSessionRequestDto,
|
||||
) -> Self {
|
||||
Self {
|
||||
mentor_id,
|
||||
mentee_id,
|
||||
topic: request.topic,
|
||||
description: request.description,
|
||||
scheduled_at: request.scheduled_at,
|
||||
duration_minutes: request.duration_minutes.unwrap_or(60),
|
||||
session_type: request.session_type.unwrap_or_else(|| "video_call".to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_status(&mut self, request: UpdateSessionStatusRequestDto) {
|
||||
self.status = request.status;
|
||||
if let Some(link) = request.meeting_link {
|
||||
self.meeting_link = Some(link);
|
||||
}
|
||||
self.updated_at = get_iso_date();
|
||||
}
|
||||
|
||||
pub fn add_feedback(&mut self, request: SessionFeedbackRequestDto) {
|
||||
self.feedback = Some(request.feedback);
|
||||
self.rating = Some(request.rating);
|
||||
self.feedback_submitted_at = Some(get_iso_date());
|
||||
self.updated_at = get_iso_date();
|
||||
}
|
||||
}
|
||||
use super::{BookSessionRequestDto, SessionFeedbackRequestDto, UpdateSessionStatusRequestDto};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
use imphnen_entities::error_dto::error::Error;
|
||||
|
||||
// Type alias for Thing
|
||||
pub type Thing = String;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct SessionSchema {
|
||||
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, // "video_call", "phone_call", "chat"
|
||||
pub status: String, // "pending", "confirmed", "completed", "cancelled", "no_show"
|
||||
pub feedback: Option<String>,
|
||||
pub rating: Option<i32>, // 1-5
|
||||
pub feedback_submitted_at: Option<DateTime<Utc>>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl Default for SessionSchema {
|
||||
fn default() -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id: Uuid::new_v4(),
|
||||
mentor_id: Uuid::new_v4(),
|
||||
mentee_id: Uuid::new_v4(),
|
||||
topic: String::new(),
|
||||
description: None,
|
||||
scheduled_at: now,
|
||||
duration_minutes: 60,
|
||||
meeting_link: None,
|
||||
session_type: "video_call".to_string(),
|
||||
status: "pending".to_string(),
|
||||
feedback: None,
|
||||
rating: None,
|
||||
feedback_submitted_at: None,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SessionSchema {
|
||||
pub fn from_book_request(
|
||||
mentor_id: Thing,
|
||||
mentee_id: Thing,
|
||||
request: BookSessionRequestDto,
|
||||
) -> Result<Self, Error> {
|
||||
let scheduled_at = DateTime::parse_from_rfc3339(&request.scheduled_at)
|
||||
.map_err(|e| Error::Validation(format!("Invalid scheduled_at format: {}", e)))?
|
||||
.with_timezone(&Utc);
|
||||
|
||||
let mentor_id = Uuid::parse_str(&mentor_id)
|
||||
.map_err(|e| Error::Validation(format!("Invalid mentor_id: {}", e)))?;
|
||||
let mentee_id = Uuid::parse_str(&mentee_id)
|
||||
.map_err(|e| Error::Validation(format!("Invalid mentee_id: {}", e)))?;
|
||||
|
||||
Ok(Self {
|
||||
mentor_id,
|
||||
mentee_id,
|
||||
topic: request.topic,
|
||||
description: request.description,
|
||||
scheduled_at,
|
||||
duration_minutes: request.duration_minutes.unwrap_or(60),
|
||||
session_type: request.session_type.unwrap_or_else(|| "video_call".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_status(&mut self, request: UpdateSessionStatusRequestDto) {
|
||||
self.status = request.status;
|
||||
if let Some(link) = request.meeting_link {
|
||||
self.meeting_link = Some(link);
|
||||
}
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
pub fn add_feedback(&mut self, request: SessionFeedbackRequestDto) {
|
||||
self.feedback = Some(request.feedback);
|
||||
self.rating = Some(request.rating);
|
||||
self.feedback_submitted_at = Some(Utc::now());
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,11 @@ use super::{
|
||||
UpdateSessionStatusResponseDto,
|
||||
};
|
||||
use axum::{http::StatusCode, response::Response};
|
||||
use chrono::{Duration, Utc};
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use imphnen_entities::ResponseSuccessDto;
|
||||
use imphnen_libs::AppState;
|
||||
use imphnen_utils::{common_response, extract_id, get_iso_date, make_thing, success_response, validate_request};
|
||||
use imphnen_utils::{common_response, success_response, validator::validate_request};
|
||||
use uuid;
|
||||
|
||||
pub struct SessionsService;
|
||||
|
||||
@@ -26,29 +27,53 @@ impl SessionsService {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
|
||||
let mentor_thing = make_thing("mentors", &mentor_id);
|
||||
let mentee_thing = make_thing("users", &user_id);
|
||||
let scheduled_at = match DateTime::parse_from_rfc3339(&dto.scheduled_at) {
|
||||
Ok(dt) => dt.with_timezone(&Utc),
|
||||
Err(e) => return common_response(StatusCode::BAD_REQUEST, &format!("Invalid scheduled_at format: {}", e)),
|
||||
};
|
||||
|
||||
let schema = SessionSchema::from_book_request(mentor_thing.clone(), mentee_thing.clone(), dto);
|
||||
let schema = SessionSchema {
|
||||
id: uuid::Uuid::new_v4(),
|
||||
mentor_id: match uuid::Uuid::parse_str(&mentor_id) {
|
||||
Ok(id) => id,
|
||||
Err(e) => return common_response(StatusCode::BAD_REQUEST, &format!("Invalid mentor ID: {}", e)),
|
||||
},
|
||||
mentee_id: match uuid::Uuid::parse_str(&user_id) {
|
||||
Ok(id) => id,
|
||||
Err(e) => return common_response(StatusCode::BAD_REQUEST, &format!("Invalid user ID: {}", e)),
|
||||
},
|
||||
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(),
|
||||
};
|
||||
|
||||
let repo = SessionsRepository::new(state);
|
||||
match repo.create_session(schema).await {
|
||||
Ok(created) => {
|
||||
let response = BookSessionResponseDto {
|
||||
id: extract_id(&created.id),
|
||||
mentor_id: extract_id(&created.mentor_id),
|
||||
mentee_id: extract_id(&created.mentee_id),
|
||||
topic: created.topic,
|
||||
description: created.description,
|
||||
scheduled_at: created.scheduled_at,
|
||||
duration_minutes: created.duration_minutes,
|
||||
session_type: created.session_type,
|
||||
status: created.status,
|
||||
created_at: created.created_at,
|
||||
};
|
||||
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(),
|
||||
};
|
||||
success_response(ResponseSuccessDto { data: response })
|
||||
}
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,17 +86,15 @@ impl SessionsService {
|
||||
_user_email: String,
|
||||
status_filter: Option<String>,
|
||||
) -> Response {
|
||||
let mentor_thing = make_thing("mentors", &mentor_id);
|
||||
|
||||
let repo = SessionsRepository::new(state);
|
||||
|
||||
// Get count and sessions
|
||||
let count = match repo.count_mentor_sessions(&mentor_thing, status_filter.clone()).await {
|
||||
let count = match repo.count_mentor_sessions(&mentor_id, status_filter.clone()).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return common_response(StatusCode::INTERNAL_SERVER_ERROR, &format!("Failed to count sessions: {}", e)),
|
||||
};
|
||||
|
||||
match repo.query_mentor_sessions(&mentor_thing, status_filter).await {
|
||||
match repo.query_mentor_sessions(&mentor_id, status_filter).await {
|
||||
Ok(sessions) => {
|
||||
let session_items: Vec<SessionListItemDto> = sessions
|
||||
.into_iter()
|
||||
@@ -97,7 +120,7 @@ impl SessionsService {
|
||||
};
|
||||
success_response(ResponseSuccessDto { data: response })
|
||||
}
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,17 +132,15 @@ impl SessionsService {
|
||||
user_id: String,
|
||||
status_filter: Option<String>,
|
||||
) -> Response {
|
||||
let user_thing = make_thing("users", &user_id);
|
||||
|
||||
let repo = SessionsRepository::new(state);
|
||||
|
||||
// Get count and sessions
|
||||
let count = match repo.count_user_sessions(&user_thing, status_filter.clone()).await {
|
||||
let count = match repo.count_user_sessions(&user_id, status_filter.clone()).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return common_response(StatusCode::INTERNAL_SERVER_ERROR, &format!("Failed to count sessions: {}", e)),
|
||||
};
|
||||
|
||||
match repo.query_user_sessions(&user_thing, status_filter).await {
|
||||
match repo.query_user_sessions(&user_id, status_filter).await {
|
||||
Ok(sessions) => {
|
||||
let session_items: Vec<SessionListItemDto> = sessions
|
||||
.into_iter()
|
||||
@@ -145,7 +166,7 @@ impl SessionsService {
|
||||
};
|
||||
success_response(ResponseSuccessDto { data: response })
|
||||
}
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,10 +174,8 @@ impl SessionsService {
|
||||
// Get Mentor Availability
|
||||
// ============================================
|
||||
pub async fn get_mentor_availability(state: &AppState, mentor_id: String) -> Response {
|
||||
let mentor_thing = make_thing("mentors", &mentor_id);
|
||||
|
||||
let repo = SessionsRepository::new(state);
|
||||
match repo.query_booked_dates(&mentor_thing).await {
|
||||
match repo.query_booked_dates(&mentor_id).await {
|
||||
Ok(booked_dates) => {
|
||||
// Generate sample availability slots (next 7 days)
|
||||
let mut slots = Vec::new();
|
||||
@@ -191,7 +210,7 @@ impl SessionsService {
|
||||
};
|
||||
success_response(ResponseSuccessDto { data: response })
|
||||
}
|
||||
Err(e) => common_response(StatusCode::NOT_FOUND, &e),
|
||||
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,27 +227,30 @@ impl SessionsService {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
|
||||
let session_thing = make_thing("sessions", &session_id);
|
||||
|
||||
let repo = SessionsRepository::new(state);
|
||||
match repo.query_session_by_id(&session_thing).await {
|
||||
match repo.query_session_by_id(&session_id).await {
|
||||
Ok(Some(mut session)) => {
|
||||
session.update_status(dto.clone());
|
||||
match repo.update_session(&session_thing, session).await {
|
||||
session.status = dto.status.clone();
|
||||
if let Some(link) = &dto.meeting_link {
|
||||
session.meeting_link = Some(link.clone());
|
||||
}
|
||||
session.updated_at = Utc::now();
|
||||
|
||||
match repo.update_session(&session_id, session).await {
|
||||
Ok(updated) => {
|
||||
let response = UpdateSessionStatusResponseDto {
|
||||
id: extract_id(&updated.id),
|
||||
id: updated.id.to_string(),
|
||||
status: updated.status,
|
||||
meeting_link: updated.meeting_link,
|
||||
updated_at: updated.updated_at,
|
||||
updated_at: updated.updated_at.to_rfc3339(),
|
||||
};
|
||||
success_response(ResponseSuccessDto { data: response })
|
||||
}
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
Ok(None) => common_response(StatusCode::NOT_FOUND, "Session not found"),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,13 +267,11 @@ impl SessionsService {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
|
||||
let session_thing = make_thing("sessions", &session_id);
|
||||
|
||||
let repo = SessionsRepository::new(state);
|
||||
match repo.query_session_by_id(&session_thing).await {
|
||||
match repo.query_session_by_id(&session_id).await {
|
||||
Ok(Some(mut session)) => {
|
||||
// Authorization: Only mentee can submit feedback
|
||||
let mentee_id = extract_id(&session.mentee_id);
|
||||
let mentee_id = session.mentee_id.to_string();
|
||||
if mentee_id != user_id {
|
||||
return common_response(
|
||||
StatusCode::FORBIDDEN,
|
||||
@@ -267,22 +287,26 @@ impl SessionsService {
|
||||
);
|
||||
}
|
||||
|
||||
session.add_feedback(dto.clone());
|
||||
match repo.update_session(&session_thing, session).await {
|
||||
session.feedback = Some(dto.feedback.clone());
|
||||
session.rating = Some(dto.rating);
|
||||
session.feedback_submitted_at = Some(Utc::now());
|
||||
session.updated_at = Utc::now();
|
||||
|
||||
match repo.update_session(&session_id, session).await {
|
||||
Ok(updated) => {
|
||||
let response = SessionFeedbackResponseDto {
|
||||
id: extract_id(&updated.id),
|
||||
id: updated.id.to_string(),
|
||||
feedback: dto.feedback,
|
||||
rating: dto.rating,
|
||||
submitted_at: updated.feedback_submitted_at.unwrap_or_else(get_iso_date),
|
||||
submitted_at: updated.feedback_submitted_at.unwrap_or(Utc::now()).to_rfc3339(),
|
||||
};
|
||||
success_response(ResponseSuccessDto { data: response })
|
||||
}
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
Ok(None) => common_response(StatusCode::NOT_FOUND, "Session not found"),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
/// Version 2 of the Dimentorin API - currently under development
|
||||
/// This module will contain all version 2 endpoints following API versioning best practices
|
||||
|
||||
use axum::Router;
|
||||
|
||||
/// Placeholder for version 2 router
|
||||
/// To be implemented when version 2 endpoints are ready
|
||||
pub fn dimentorin_v2_router() -> Router {
|
||||
Router::new()
|
||||
// Version 2 endpoints will be added here following the same pattern as v1
|
||||
}
|
||||
|
||||
// Re-export the v1 router for backward compatibility
|
||||
pub use crate::v1::dimentorin_router;
|
||||
/// Version 2 of the Dimentorin API - currently under development
|
||||
/// This module will contain all version 2 endpoints following API versioning best practices
|
||||
|
||||
use axum::Router;
|
||||
|
||||
/// Placeholder for version 2 router
|
||||
/// To be implemented when version 2 endpoints are ready
|
||||
pub fn dimentorin_v2_router() -> Router {
|
||||
Router::new()
|
||||
// Version 2 endpoints will be added here following the same pattern as v1
|
||||
}
|
||||
|
||||
// Re-export the v1 router for backward compatibility
|
||||
pub use crate::v1::dimentorin_router;
|
||||
|
||||
Reference in New Issue
Block a user