fix(dimentorin): resolve mentor profile id to user id before booking session

This commit is contained in:
asepharyana
2026-08-04 18:34:13 +07:00
parent 67d3f2fced
commit 7d1078f52a
24 changed files with 437 additions and 1 deletions
+2
View File
@@ -29,6 +29,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
drop_and_create_table(&db, builder, "app_mentors", auth::mentors::Entity).await?;
drop_and_create_table(&db, builder, "app_sessions", auth::sessions::Entity)
.await?;
drop_and_create_table(&db, builder, "app_articles", common::articles::Entity)
.await?;
drop_and_create_table(&db, builder, "events", common::events::Entity).await?;
drop_and_create_table(&db, builder, "testimonials", common::testimonials::Entity)
@@ -0,0 +1,73 @@
use async_trait::async_trait;
use paginator_utils::{PaginatorResponse, PaginatorResponseMeta};
use std::sync::Arc;
use uuid::Uuid;
use super::super::domain::article::ArticleEntity;
use super::super::domain::article_types::{ArticleDetail, ArticleListItem, CreateArticleCommand};
use super::super::domain::repository::ArticleRepository;
use super::super::domain::service::ArticleService;
use imphnen_utils::AppError;
pub struct ArticleServiceImpl {
repo: Arc<dyn ArticleRepository>,
}
impl ArticleServiceImpl {
pub fn new(repo: Arc<dyn ArticleRepository>) -> Self {
Self { repo }
}
}
#[async_trait]
impl ArticleService for ArticleServiceImpl {
async fn list(
&self,
page: u64,
per_page: u64,
category: Option<String>,
) -> Result<PaginatorResponse<ArticleListItem>, AppError> {
let result = self.repo.find_all_paginated(page, per_page, category).await?;
let items: Vec<ArticleListItem> = result
.data
.into_iter()
.map(ArticleListItem::from)
.collect();
Ok(PaginatorResponse {
data: items,
meta: result.meta,
})
}
async fn get_by_id(&self, id: Uuid) -> Result<ArticleDetail, AppError> {
let entity = self.repo.find_by_id(id).await?;
Ok(ArticleDetail::from(entity))
}
async fn get_by_slug(&self, slug: &str) -> Result<ArticleDetail, AppError> {
let entity = self.repo.find_by_slug(slug).await?;
Ok(ArticleDetail::from(entity))
}
async fn categories(&self) -> Result<Vec<String>, AppError> {
self.repo.find_categories().await
}
async fn create(&self, cmd: CreateArticleCommand) -> Result<ArticleDetail, AppError> {
let entity = ArticleEntity {
id: Uuid::new_v4(),
title: cmd.title,
slug: cmd.slug,
category: cmd.category,
excerpt: cmd.excerpt,
content: cmd.content,
cover_url: cmd.cover_url,
author_name: cmd.author_name,
is_published: true,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
self.repo.create(entity.clone()).await?;
Ok(ArticleDetail::from(entity))
}
}
@@ -0,0 +1,3 @@
pub mod article_service;
pub use article_service::ArticleServiceImpl;
@@ -0,0 +1,17 @@
use chrono::{DateTime, Utc};
use uuid::Uuid;
#[derive(Clone, Debug)]
pub struct ArticleEntity {
pub id: Uuid,
pub title: String,
pub slug: String,
pub category: String,
pub excerpt: String,
pub content: String,
pub cover_url: Option<String>,
pub author_name: Option<String>,
pub is_published: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
@@ -0,0 +1,75 @@
use chrono::{DateTime, Utc};
use uuid::Uuid;
use super::article::ArticleEntity;
#[derive(Clone, Debug)]
pub struct ArticleListItem {
pub id: Uuid,
pub title: String,
pub slug: String,
pub category: String,
pub excerpt: String,
pub cover_url: Option<String>,
pub author_name: Option<String>,
pub created_at: DateTime<Utc>,
}
#[derive(Clone, Debug)]
pub struct ArticleDetail {
pub id: Uuid,
pub title: String,
pub slug: String,
pub category: String,
pub excerpt: String,
pub content: String,
pub cover_url: Option<String>,
pub author_name: Option<String>,
pub is_published: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Clone, Debug)]
pub struct CreateArticleCommand {
pub title: String,
pub slug: String,
pub category: String,
pub excerpt: String,
pub content: String,
pub cover_url: Option<String>,
pub author_name: Option<String>,
}
impl From<ArticleEntity> for ArticleListItem {
fn from(e: ArticleEntity) -> Self {
Self {
id: e.id,
title: e.title,
slug: e.slug,
category: e.category,
excerpt: e.excerpt,
cover_url: e.cover_url,
author_name: e.author_name,
created_at: e.created_at,
}
}
}
impl From<ArticleEntity> for ArticleDetail {
fn from(e: ArticleEntity) -> Self {
Self {
id: e.id,
title: e.title,
slug: e.slug,
category: e.category,
excerpt: e.excerpt,
content: e.content,
cover_url: e.cover_url,
author_name: e.author_name,
is_published: e.is_published,
created_at: e.created_at,
updated_at: e.updated_at,
}
}
}
@@ -0,0 +1,4 @@
pub mod article;
pub mod article_types;
pub mod repository;
pub mod service;
@@ -0,0 +1,20 @@
use async_trait::async_trait;
use paginator_utils::PaginatorResponse;
use uuid::Uuid;
use super::article::ArticleEntity;
use imphnen_utils::AppError;
#[async_trait]
pub trait ArticleRepository: Send + Sync {
async fn find_all_paginated(
&self,
page: u64,
per_page: u64,
category: Option<String>,
) -> Result<PaginatorResponse<ArticleEntity>, AppError>;
async fn find_by_id(&self, id: Uuid) -> Result<ArticleEntity, AppError>;
async fn find_by_slug(&self, slug: &str) -> Result<ArticleEntity, AppError>;
async fn find_categories(&self) -> Result<Vec<String>, AppError>;
async fn create(&self, entity: ArticleEntity) -> Result<Uuid, AppError>;
}
@@ -0,0 +1,23 @@
use async_trait::async_trait;
use paginator_utils::PaginatorResponse;
use uuid::Uuid;
use super::article_types::{ArticleDetail, ArticleListItem, CreateArticleCommand};
use imphnen_utils::AppError;
#[async_trait]
pub trait ArticleService: Send + Sync {
async fn list(
&self,
page: u64,
per_page: u64,
category: Option<String>,
) -> Result<PaginatorResponse<ArticleListItem>, AppError>;
async fn get_by_id(&self, id: Uuid) -> Result<ArticleDetail, AppError>;
async fn get_by_slug(&self, slug: &str) -> Result<ArticleDetail, AppError>;
async fn categories(&self) -> Result<Vec<String>, AppError>;
async fn create(
&self,
cmd: CreateArticleCommand,
) -> Result<ArticleDetail, AppError>;
}
@@ -0,0 +1,2 @@
pub mod postgres_article_queries;
pub mod postgres_article_repository;
@@ -0,0 +1,92 @@
use super::super::super::domain::article::ArticleEntity;
use imphnen_entities::seaorm::common::articles::{
Column as ArticleColumn, Entity as ArticlesEntity, Model as ArticleModel,
};
use imphnen_utils::AppError;
use paginator_utils::{PaginatorResponse, PaginatorResponseMeta};
use sea_orm::prelude::*;
use sea_orm::{EntityTrait, Order, PaginatorTrait, QueryFilter, QueryOrder};
use std::sync::Arc;
pub fn model_to_entity(model: ArticleModel) -> ArticleEntity {
ArticleEntity {
id: model.id,
title: model.title,
slug: model.slug,
category: model.category,
excerpt: model.excerpt,
content: model.content,
cover_url: model.cover_url,
author_name: model.author_name,
is_published: model.is_published,
created_at: model.created_at,
updated_at: model.updated_at,
}
}
pub async fn find_all_paginated(
db: &Arc<DatabaseConnection>,
page: u64,
per_page: u64,
category: Option<String>,
) -> Result<PaginatorResponse<ArticleEntity>, AppError> {
let mut query = ArticlesEntity::find().filter(ArticleColumn::IsPublished.eq(true));
if let Some(cat) = category.filter(|c| !c.is_empty()) {
query = query.filter(ArticleColumn::Category.eq(cat));
}
query = query.order_by(ArticleColumn::CreatedAt, Order::Desc);
let paginator = query.paginate(db.as_ref(), per_page);
let total = paginator
.num_items()
.await
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
let articles = paginator
.fetch_page(page.saturating_sub(1))
.await
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
let data = articles.into_iter().map(model_to_entity).collect();
let meta = PaginatorResponseMeta::new(page, per_page, total as u32);
Ok(PaginatorResponse { data, meta })
}
pub async fn find_by_id(
db: &Arc<DatabaseConnection>,
id: Uuid,
) -> Result<ArticleEntity, AppError> {
let model = ArticlesEntity::find_by_id(id)
.filter(ArticleColumn::IsPublished.eq(true))
.one(db.as_ref())
.await
.map_err(|e| AppError::InternalServerError(e.to_string()))?
.ok_or_else(|| AppError::NotFoundError("Article not found".to_string()))?;
Ok(model_to_entity(model))
}
pub async fn find_by_slug(
db: &Arc<DatabaseConnection>,
slug: &str,
) -> Result<ArticleEntity, AppError> {
let model = ArticlesEntity::find()
.filter(ArticleColumn::Slug.eq(slug))
.filter(ArticleColumn::IsPublished.eq(true))
.one(db.as_ref())
.await
.map_err(|e| AppError::InternalServerError(e.to_string()))?
.ok_or_else(|| AppError::NotFoundError("Article not found".to_string()))?;
Ok(model_to_entity(model))
}
pub async fn find_categories(db: &Arc<DatabaseConnection>) -> Result<Vec<String>, AppError> {
let rows = ArticlesEntity::find()
.select_only()
.column(ArticleColumn::Category)
.distinct()
.all(db.as_ref())
.await
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
Ok(rows.into_iter().map(|r| r.category).collect())
}
@@ -0,0 +1,67 @@
use super::postgres_article_queries::{
find_all_paginated, find_by_id, find_by_slug, find_categories,
};
use super::super::super::domain::article::ArticleEntity;
use super::super::super::domain::repository::ArticleRepository;
use imphnen_entities::seaorm::common::articles::{ActiveModel, Column as ArticleColumn};
use imphnen_utils::AppError;
use async_trait::async_trait;
use paginator_utils::PaginatorResponse;
use sea_orm::{ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, Set};
use std::sync::Arc;
use uuid::Uuid;
pub struct PostgresArticleRepository {
db: Arc<DatabaseConnection>,
}
impl PostgresArticleRepository {
pub fn new(db: DatabaseConnection) -> Self {
Self { db: Arc::new(db) }
}
}
#[async_trait]
impl ArticleRepository for PostgresArticleRepository {
async fn find_all_paginated(
&self,
page: u64,
per_page: u64,
category: Option<String>,
) -> Result<PaginatorResponse<ArticleEntity>, AppError> {
find_all_paginated(&self.db, page, per_page, category).await
}
async fn find_by_id(&self, id: Uuid) -> Result<ArticleEntity, AppError> {
find_by_id(&self.db, id).await
}
async fn find_by_slug(&self, slug: &str) -> Result<ArticleEntity, AppError> {
find_by_slug(&self.db, slug).await
}
async fn find_categories(&self) -> Result<Vec<String>, AppError> {
find_categories(&self.db).await
}
async fn create(&self, entity: ArticleEntity) -> Result<Uuid, AppError> {
let active = ActiveModel {
id: sea_orm::ActiveValue::Set(entity.id),
title: Set(entity.title),
slug: Set(entity.slug),
category: Set(entity.category),
excerpt: Set(entity.excerpt),
content: Set(entity.content),
cover_url: Set(entity.cover_url),
author_name: Set(entity.author_name),
is_published: Set(entity.is_published),
created_at: Set(entity.created_at),
updated_at: Set(entity.updated_at),
};
active
.insert(self.db.as_ref())
.await
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
Ok(entity.id)
}
}
+5
View File
@@ -0,0 +1,5 @@
pub mod domain;
pub mod application;
pub mod infrastructure;
pub use infrastructure::http::routes::{articles_public_routes, articles_protected_routes};
@@ -12,7 +12,13 @@ use axum::{
use imphnen_libs::{ValidatedJson, decode_access_token};
use imphnen_utils::AppError;
use imphnen_utils::ApiSuccess;
use imphnen_entities::seaorm::auth::mentors::{
Column as MentorColumn, Entity as MentorsEntity,
};
use imphnen_libs::AppState;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
use std::sync::Arc;
use uuid::Uuid;
fn extract_user_id(headers: &HeaderMap) -> Result<String, AppError> {
let token = headers
@@ -43,14 +49,23 @@ fn extract_user_id(headers: &HeaderMap) -> Result<String, AppError> {
)]
pub async fn post_book_session(
headers: HeaderMap,
Extension(state): Extension<AppState>,
Extension(service): Extension<Arc<dyn SessionService>>,
Path(mentor_id): Path<String>,
ValidatedJson(dto): ValidatedJson<BookSessionRequestDto>,
) -> Result<impl IntoResponse, AppError> {
let user_id = extract_user_id(&headers)?;
let mentor_uuid = Uuid::parse_str(&mentor_id).map_err(|_| {
AppError::BadRequestError("Invalid mentor ID format".to_string())
})?;
// Resolve mentor profile id -> user id (sessions.mentor_id FK ke app_users)
let mentor = MentorsEntity::find_by_id(mentor_uuid)
.one(&state.postgres_connection.conn)
.await?
.ok_or_else(|| AppError::NotFoundError("Mentor not found".to_string()))?;
let resp = BookSessionResponseDto::from(
service
.book_session(mentor_id, user_id, dto.into())
.book_session(mentor.user_id.to_string(), user_id, dto.into())
.await?,
);
Ok(ApiSuccess(resp))
@@ -0,0 +1,37 @@
use chrono::{DateTime, Utc};
use sea_orm::entity::prelude::*;
use uuid::Uuid;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "app_articles")]
pub struct Model {
#[sea_orm(primary_key, default = "gen_random_uuid()", auto_increment = false)]
pub id: Uuid,
pub title: String,
pub slug: String,
pub category: String,
pub excerpt: String,
pub content: String,
#[sea_orm(nullable)]
pub cover_url: Option<String>,
#[sea_orm(nullable)]
pub author_name: Option<String>,
pub is_published: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
@@ -1,3 +1,4 @@
pub mod articles;
pub mod audit_log;
pub mod enum_impls;
pub mod enums;