fix(dimentorin): resolve mentor profile id to user id before booking session
This commit is contained in:
@@ -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;
|
||||
+92
@@ -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())
|
||||
}
|
||||
+67
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user