use sqlx::PgPool; use uuid::Uuid; use chrono::NaiveDate; use crate::{ error::{ApiError, Result}, models::{Bulletin, CreateBulletinRequest}, utils::sanitize::strip_html_tags, }; /// List bulletins with pagination pub async fn list( pool: &PgPool, page: i32, per_page: i64, active_only: bool, ) -> Result<(Vec, i64)> { let offset = ((page - 1) as i64) * per_page; // Get bulletins with pagination let bulletins = if active_only { sqlx::query_as!( Bulletin, r#"SELECT id, title, date, url, pdf_url, is_active, pdf_file, sabbath_school, divine_worship, scripture_reading, sunset, cover_image, pdf_path, created_at, updated_at FROM bulletins WHERE is_active = true ORDER BY date DESC LIMIT $1 OFFSET $2"#, per_page, offset ) } else { sqlx::query_as!( Bulletin, r#"SELECT id, title, date, url, pdf_url, is_active, pdf_file, sabbath_school, divine_worship, scripture_reading, sunset, cover_image, pdf_path, created_at, updated_at FROM bulletins ORDER BY date DESC LIMIT $1 OFFSET $2"#, per_page, offset ) } .fetch_all(pool) .await .map_err(|e| { tracing::error!("Failed to list bulletins: {}", e); ApiError::DatabaseError(e) })?; // Get total count let total = if active_only { sqlx::query_scalar!( "SELECT COUNT(*) FROM bulletins WHERE is_active = true" ) } else { sqlx::query_scalar!( "SELECT COUNT(*) FROM bulletins" ) } .fetch_one(pool) .await .map_err(|e| { tracing::error!("Failed to count bulletins: {}", e); ApiError::DatabaseError(e) })? .unwrap_or(0); Ok((bulletins, total)) } /// Get current bulletin (active and date <= today) pub async fn get_current(pool: &PgPool) -> Result> { sqlx::query_as!( Bulletin, r#"SELECT id, title, date, url, pdf_url, is_active, pdf_file, sabbath_school, divine_worship, scripture_reading, sunset, cover_image, pdf_path, created_at, updated_at FROM bulletins WHERE is_active = true AND date <= (NOW() AT TIME ZONE 'America/New_York')::date ORDER BY date DESC LIMIT 1"# ) .fetch_optional(pool) .await .map_err(|e| { tracing::error!("Failed to get current bulletin: {}", e); ApiError::DatabaseError(e) }) } /// Get next bulletin (active and date > today) pub async fn get_next(pool: &PgPool) -> Result> { sqlx::query_as!( Bulletin, r#"SELECT id, title, date, url, pdf_url, is_active, pdf_file, sabbath_school, divine_worship, scripture_reading, sunset, cover_image, pdf_path, created_at, updated_at FROM bulletins WHERE is_active = true AND date > (NOW() AT TIME ZONE 'America/New_York')::date ORDER BY date ASC LIMIT 1"# ) .fetch_optional(pool) .await .map_err(|e| { tracing::error!("Failed to get next bulletin: {}", e); ApiError::DatabaseError(e) }) } /// Get bulletin by ID pub async fn get_by_id(pool: &PgPool, id: &Uuid) -> Result> { sqlx::query_as!( Bulletin, r#"SELECT id, title, date, url, pdf_url, is_active, pdf_file, sabbath_school, divine_worship, scripture_reading, sunset, cover_image, pdf_path, created_at, updated_at FROM bulletins WHERE id = $1"#, id ) .fetch_optional(pool) .await .map_err(|e| { tracing::error!("Failed to get bulletin by id {}: {}", id, e); ApiError::DatabaseError(e) }) } /// Get bulletin by date pub async fn get_by_date(pool: &PgPool, date: NaiveDate) -> Result> { sqlx::query_as!( Bulletin, r#"SELECT id, title, date, url, pdf_url, is_active, pdf_file, sabbath_school, divine_worship, scripture_reading, sunset, cover_image, pdf_path, created_at, updated_at FROM bulletins WHERE date = $1 AND is_active = true ORDER BY created_at DESC LIMIT 1"#, date ) .fetch_optional(pool) .await .map_err(|e| { tracing::error!("Failed to get bulletin by date {}: {}", date, e); ApiError::DatabaseError(e) }) } /// Create new bulletin pub async fn create(pool: &PgPool, bulletin: &CreateBulletinRequest) -> Result { let id = Uuid::new_v4(); let clean_title = strip_html_tags(&bulletin.title); sqlx::query_as!( Bulletin, r#"INSERT INTO bulletins ( id, title, date, url, pdf_url, is_active, pdf_file, sabbath_school, divine_worship, scripture_reading, sunset, cover_image, pdf_path, created_at, updated_at ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, NOW(), NOW() ) RETURNING *"#, id, clean_title, bulletin.date, bulletin.url, bulletin.pdf_url, bulletin.is_active.unwrap_or(true), bulletin.pdf_file, bulletin.sabbath_school, bulletin.divine_worship, bulletin.scripture_reading, bulletin.sunset, bulletin.cover_image, bulletin.pdf_path ) .fetch_one(pool) .await .map_err(|e| { tracing::error!("Failed to create bulletin: {}", e); match e { sqlx::Error::Database(db_err) if db_err.constraint().is_some() => { ApiError::duplicate_entry("Bulletin", &bulletin.date) } _ => ApiError::DatabaseError(e) } }) } /// Update bulletin pub async fn update(pool: &PgPool, id: &Uuid, bulletin: &CreateBulletinRequest) -> Result { let clean_title = strip_html_tags(&bulletin.title); sqlx::query_as!( Bulletin, r#"UPDATE bulletins SET title = $2, date = $3, url = $4, pdf_url = $5, is_active = $6, pdf_file = $7, sabbath_school = $8, divine_worship = $9, scripture_reading = $10, sunset = $11, cover_image = $12, pdf_path = $13, updated_at = NOW() WHERE id = $1 RETURNING *"#, id, clean_title, bulletin.date, bulletin.url, bulletin.pdf_url, bulletin.is_active.unwrap_or(true), bulletin.pdf_file, bulletin.sabbath_school, bulletin.divine_worship, bulletin.scripture_reading, bulletin.sunset, bulletin.cover_image, bulletin.pdf_path ) .fetch_one(pool) .await .map_err(|e| { tracing::error!("Failed to update bulletin {}: {}", id, e); match e { sqlx::Error::RowNotFound => ApiError::bulletin_not_found(id), sqlx::Error::Database(db_err) if db_err.constraint().is_some() => { ApiError::duplicate_entry("Bulletin", &bulletin.date) } _ => ApiError::DatabaseError(e) } }) } /// Delete bulletin pub async fn delete(pool: &PgPool, id: &Uuid) -> Result<()> { let result = sqlx::query!( "DELETE FROM bulletins WHERE id = $1", id ) .execute(pool) .await .map_err(|e| { tracing::error!("Failed to delete bulletin {}: {}", id, e); ApiError::DatabaseError(e) })?; if result.rows_affected() == 0 { return Err(ApiError::bulletin_not_found(id)); } Ok(()) }