// SHARED PROCESSING FUNCTIONS for bulletins handler use crate::{ error::Result, models::Bulletin, utils::db_operations::BibleVerseOperations, services::HymnalService, }; use regex::Regex; /// Process multiple bulletins with shared logic pub async fn process_bulletins_batch( pool: &sqlx::PgPool, bulletins: &mut [Bulletin] ) -> Result<()> { for bulletin in bulletins.iter_mut() { process_single_bulletin(pool, bulletin).await?; } Ok(()) } /// Process a single bulletin with all required transformations pub async fn process_single_bulletin( pool: &sqlx::PgPool, bulletin: &mut Bulletin ) -> Result<()> { // Process scripture reading to include full verse text bulletin.scripture_reading = process_scripture_reading(pool, &bulletin.scripture_reading).await?; // Process hymn references in worship content if let Some(ref worship_content) = bulletin.divine_worship { bulletin.divine_worship = Some(process_hymn_references(pool, worship_content).await?); } // Process hymn references in sabbath school content if let Some(ref ss_content) = bulletin.sabbath_school { bulletin.sabbath_school = Some(process_hymn_references(pool, ss_content).await?); } // Ensure sunset field compatibility if bulletin.sunset.is_none() { bulletin.sunset = Some("TBA".to_string()); } Ok(()) } /// Process scripture reading field to lookup and include full verse text async fn process_scripture_reading( pool: &sqlx::PgPool, scripture: &Option, ) -> Result> { let Some(scripture_text) = scripture else { return Ok(None); }; // If it already looks like it has full verse text (long), return as-is if scripture_text.len() > 50 { return Ok(Some(scripture_text.clone())); } // Try to find the verse(s) using existing search functionality // Allow up to 10 verses for ranges like "Matt 1:21-23" match BibleVerseOperations::search(pool, scripture_text, 10).await { Ok(verses) if !verses.is_empty() => { if verses.len() == 1 { // Single verse - format as before let verse = &verses[0]; Ok(Some(format!("{} - {}", verse.text, scripture_text))) } else { // Multiple verses - combine them let combined_text = verses .iter() .map(|v| v.text.as_str()) .collect::>() .join(" "); Ok(Some(format!("{} - {}", combined_text, scripture_text))) } }, _ => { // If no match found, return original text Ok(Some(scripture_text.clone())) } } } /// Process hymn references in bulletin content to include titles /// Looks for patterns like #319, Hymn 319, No. 319 and adds hymn titles pub async fn process_hymn_references( pool: &sqlx::PgPool, content: &str, ) -> Result { // Create regex patterns to match hymn references let hymn_patterns = vec![ // #319 Regex::new(r"#(\d{1,3})").unwrap(), // Hymn 319 (case insensitive) Regex::new(r"(?i)hymn\s+(\d{1,3})").unwrap(), // No. 319 Regex::new(r"(?i)no\.\s*(\d{1,3})").unwrap(), // Number 319 Regex::new(r"(?i)number\s+(\d{1,3})").unwrap(), ]; let mut result = content.to_string(); // Default to 1985 hymnal (most common) let default_hymnal = "sda-1985"; // Process each pattern for pattern in hymn_patterns { let mut matches_to_replace = Vec::new(); // Find all matches for this pattern for capture in pattern.captures_iter(&result) { if let Some(number_match) = capture.get(1) { if let Ok(hymn_number) = number_match.as_str().parse::() { // Try to get hymn from 1985 hymnal first, then 1941 let hymn_title = match HymnalService::get_hymn_by_number(pool, default_hymnal, hymn_number).await { Ok(Some(hymn)) => Some(hymn.title), _ => { // Try 1941 hymnal as fallback match HymnalService::get_hymn_by_number(pool, "sda-1941", hymn_number).await { Ok(Some(hymn)) => Some(hymn.title), _ => None, } } }; if let Some(title) = hymn_title { let full_match = capture.get(0).unwrap(); matches_to_replace.push(( full_match.start(), full_match.end(), format!("{} - {}", full_match.as_str(), title) )); } } } } // Apply replacements in reverse order to maintain string indices matches_to_replace.reverse(); for (start, end, replacement) in matches_to_replace { result.replace_range(start..end, &replacement); } } Ok(result) }