use crate::error::AppError; use crate::models::common::{EventType, SubscriptionLevel, TargetType}; use crate::models::notifications::NotificationSubscription; 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, utoipa::ToSchema)] pub struct CreateSubscriptionParams { pub workspace_id: Option, pub repo_id: Option, pub target_type: TargetType, pub target_id: Option, pub event_types: Vec, pub channels: Vec, pub level: SubscriptionLevel, } #[derive(Debug, Deserialize, Serialize, utoipa::ToSchema)] pub struct UpdateSubscriptionParams { pub event_types: Option>, pub channels: Option>, pub level: Option, pub muted: Option, pub muted_until: Option>, } impl NotificationSubscription { pub async fn find_by_id( pool: &sqlx::PgPool, id: Uuid, user_id: Uuid, ) -> Result, AppError> { sqlx::query_as::<_, NotificationSubscription>( "SELECT id, user_id, workspace_id, repo_id, target_type, target_id, event_types, \ channels, level, muted, muted_until, created_at, updated_at \ FROM notification_subscription 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::<_, NotificationSubscription>( "SELECT id, user_id, workspace_id, repo_id, target_type, target_id, event_types, \ channels, level, muted, muted_until, created_at, updated_at \ FROM notification_subscription 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_subscriptions( &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); NotificationSubscription::list_for_user(self.ctx.db.reader(), user_id, limit, offset).await } pub async fn create_subscription( &self, session: &Session, params: CreateSubscriptionParams, ) -> Result { let user_id = session.user().ok_or(AppError::Unauthorized)?; let id = Uuid::now_v7(); let now = Utc::now(); sqlx::query_as::<_, NotificationSubscription>( "INSERT INTO notification_subscription \ (id, user_id, workspace_id, repo_id, target_type, target_id, event_types, channels, level, muted, muted_until, created_at, updated_at) \ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, false, NULL, $10, $10) \ RETURNING id, user_id, workspace_id, repo_id, target_type, target_id, event_types, \ channels, level, muted, muted_until, created_at, updated_at", ) .bind(id) .bind(user_id) .bind(params.workspace_id) .bind(params.repo_id) .bind(params.target_type.as_str()) .bind(params.target_id) .bind(¶ms.event_types) .bind(¶ms.channels) .bind(params.level.as_str()) .bind(now) .fetch_one(self.ctx.db.writer()) .await .map_err(AppError::Database) } pub async fn update_subscription( &self, session: &Session, subscription_id: Uuid, params: UpdateSubscriptionParams, ) -> Result { let user_id = session.user().ok_or(AppError::Unauthorized)?; let now = Utc::now(); let existing = NotificationSubscription::find_by_id(self.ctx.db.reader(), subscription_id, user_id) .await? .ok_or(AppError::NotFound("subscription not found".into()))?; let event_types = params.event_types.unwrap_or(existing.event_types); let channels = params.channels.unwrap_or(existing.channels); let level = params.level.unwrap_or(existing.level); let muted = params.muted.unwrap_or(existing.muted); let muted_until = params.muted_until.or(existing.muted_until); sqlx::query( "UPDATE notification_subscription \ SET event_types = $1, channels = $2, level = $3, muted = $4, muted_until = $5, updated_at = $6 \ WHERE id = $7 AND user_id = $8", ) .bind(&event_types) .bind(&channels) .bind(level.as_str()) .bind(muted) .bind(muted_until) .bind(now) .bind(subscription_id) .bind(user_id) .execute(self.ctx.db.writer()) .await .map_err(AppError::Database)?; NotificationSubscription::find_by_id(self.ctx.db.reader(), subscription_id, user_id) .await? .ok_or(AppError::InternalServerError( "failed to fetch updated subscription".into(), )) } pub async fn delete_subscription( &self, session: &Session, subscription_id: Uuid, ) -> Result<(), AppError> { let user_id = session.user().ok_or(AppError::Unauthorized)?; let result = sqlx::query("DELETE FROM notification_subscription WHERE id = $1 AND user_id = $2") .bind(subscription_id) .bind(user_id) .execute(self.ctx.db.writer()) .await .map_err(AppError::Database)?; if result.rows_affected() == 0 { return Err(AppError::NotFound("subscription not found".into())); } Ok(()) } }