use paginator_utils::PaginatorResponse; use chrono::Utc; use uuid::Uuid; use super::super::domain::{ CreateMaterialCommand, MaterialEntity, MaterialListItem, MaterialRepository, MaterialService, UpdateMaterialCommand, }; use crate::materials::domain::{MaterialRepository as _}; use imphnen_utils::AppError; pub struct MaterialServiceImpl { repo: Box, } impl MaterialServiceImpl { pub fn new(repo: Box) -> Self { Self { repo } } fn slugify(title: &str) -> String { let slug: String = title .to_lowercase() .chars() .map(|c| { if c.is_ascii_alphanumeric() { c } else if c.is_whitespace() { '-' } else { '-' } }) .collect(); slug.trim_matches('-').to_string() } } #[async_trait::async_trait] impl MaterialService for MaterialServiceImpl { async fn list_materials( &self, page: u64, per_page: u64, category: Option, published_only: bool, ) -> Result, AppError> { let res = self .repo .find_all_paginated(page, per_page, category, published_only) .await?; Ok(PaginatorResponse { data: res .data .into_iter() .map(|e| MaterialListItem::from_entity(&e)) .collect(), meta: res.meta, }) } async fn get_material_by_id(&self, id: Uuid) -> Result { self.repo.find_by_id(id).await } async fn get_material_by_slug(&self, slug: &str) -> Result { self.repo.find_by_slug(slug).await } async fn list_categories(&self) -> Result, AppError> { self.repo.find_categories().await } async fn create_material( &self, cmd: CreateMaterialCommand, ) -> Result { let now = Utc::now(); let entity = MaterialEntity { id: Uuid::new_v4(), mentor_id: cmd.mentor_id, title: cmd.title.clone(), slug: format!("{}-{}", Self::slugify(&cmd.title), Uuid::new_v4().to_string()[..8].to_string()), category: cmd.category, description: cmd.description, content: cmd.content, cover_url: cmd.cover_url, is_published: true, created_at: now, updated_at: now, }; self.repo.create(entity.clone()).await?; Ok(MaterialListItem::from_entity(&entity)) } async fn update_material( &self, id: Uuid, actor_id: Uuid, cmd: UpdateMaterialCommand, ) -> Result { let existing = self.repo.find_by_id(id).await?; if existing.mentor_id != actor_id { return Err(AppError::ForbiddenError( "Only the author can update this material".into(), )); } // do not change mentor on update let entity = MaterialEntity { id, mentor_id: existing.mentor_id, title: cmd.title.unwrap_or(existing.title), slug: existing.slug, category: cmd.category.unwrap_or(existing.category), description: cmd.description.unwrap_or(existing.description), content: cmd.content.unwrap_or(existing.content), cover_url: cmd.cover_url.or(existing.cover_url), is_published: cmd.is_published.unwrap_or(existing.is_published), created_at: existing.created_at, updated_at: Utc::now(), }; self.repo.update(id, entity.clone()).await?; Ok(MaterialListItem::from_entity(&entity)) } async fn delete_material(&self, id: Uuid, actor_id: Uuid) -> Result<(), AppError> { let existing = self.repo.find_by_id(id).await?; if existing.mentor_id != actor_id { return Err(AppError::ForbiddenError( "Only the author can delete this material".into(), )); } self.repo.delete(id).await } }