use crate::error::AppError; use crate::models::common::{DeliveryChannel, NotificationType, TargetType}; use crate::models::notifications::NotificationBlock; use crate::service::NotificationService; use crate::session::Session; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; use super::util::clamp_limit_offset; #[derive(Debug, Deserialize, Serialize)] pub struct CreateBlockParams { pub workspace_id: Option, pub repo_id: Option, pub target_type: TargetType, pub target_id: Option, pub notification_type: Option, pub channel: Option, pub reason: Option, pub expires_at: Option>, } impl NotificationBlock { pub async fn find_by_id( pool: &sqlx::PgPool, id: Uuid, user_id: Uuid, ) -> Result, AppError> { sqlx::query_as::<_, NotificationBlock>( "SELECT id, user_id, workspace_id, repo_id, target_type, target_id, notification_type, \ channel, reason, expires_at, created_at, updated_at \ FROM notification_block WHERE id = $1 AND user_id = $2", ) .bind(id) .bind(user_id) .fetch_optional(pool) .await .map_err(AppError::Database) } pub async fn list_for_user( pool: &sqlx::PgPool, user_id: Uuid, limit: i64, offset: i64, ) -> Result, AppError> { sqlx::query_as::<_, NotificationBlock>( "SELECT id, user_id, workspace_id, repo_id, target_type, target_id, notification_type, \ channel, reason, expires_at, created_at, updated_at \ FROM notification_block WHERE user_id = $1 \ ORDER BY created_at DESC LIMIT $2 OFFSET $3", ) .bind(user_id) .bind(limit) .bind(offset) .fetch_all(pool) .await .map_err(AppError::Database) } } impl NotificationService { pub async fn list_blocks( &self, session: &Session, limit: i64, offset: i64, ) -> Result, AppError> { let user_id = session.user().ok_or(AppError::Unauthorized)?; let (limit, offset) = clamp_limit_offset(limit, offset); NotificationBlock::list_for_user(self.ctx.db.reader(), user_id, limit, offset).await } pub async fn create_block( &self, session: &Session, params: CreateBlockParams, ) -> Result { let user_id = session.user().ok_or(AppError::Unauthorized)?; let id = Uuid::now_v7(); let now = Utc::now(); sqlx::query( "INSERT INTO notification_block \ (id, user_id, workspace_id, repo_id, target_type, target_id, notification_type, channel, reason, expires_at, created_at, updated_at) \ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $11)", ) .bind(id) .bind(user_id) .bind(params.workspace_id) .bind(params.repo_id) .bind(params.target_type.as_str()) .bind(params.target_id) .bind(params.notification_type.map(|t| t.as_str())) .bind(params.channel.map(|c| c.as_str())) .bind(params.reason) .bind(params.expires_at) .bind(now) .execute(self.ctx.db.writer()) .await .map_err(AppError::Database)?; NotificationBlock::find_by_id(self.ctx.db.reader(), id, user_id) .await? .ok_or(AppError::InternalServerError( "failed to fetch created block".into(), )) } pub async fn delete_block(&self, session: &Session, block_id: Uuid) -> Result<(), AppError> { let user_id = session.user().ok_or(AppError::Unauthorized)?; let result = sqlx::query("DELETE FROM notification_block WHERE id = $1 AND user_id = $2") .bind(block_id) .bind(user_id) .execute(self.ctx.db.writer()) .await .map_err(AppError::Database)?; if result.rows_affected() == 0 { return Err(AppError::NotFound("block not found".into())); } Ok(()) } }