// SHARED PROCESSING FUNCTIONS for bulletins handler use crate::{ error::Result, models::Bulletin, services::HymnalService, }; use regex::Regex; /// Process multiple bulletins with shared logic (optimized to prevent N+1 queries) pub async fn process_bulletins_batch( pool: &sqlx::PgPool, bulletins: &mut [Bulletin] ) -> Result<()> { // Collect all hymn numbers across all bulletins first for batch lookup let mut all_hymn_numbers = std::collections::HashSet::new(); for bulletin in bulletins.iter() { // Extract hymn numbers from divine_worship content if let Some(ref worship_content) = bulletin.divine_worship { let hymn_numbers = extract_hymn_numbers_from_content(worship_content); all_hymn_numbers.extend(hymn_numbers); } // Extract hymn numbers from sabbath_school content if let Some(ref ss_content) = bulletin.sabbath_school { let hymn_numbers = extract_hymn_numbers_from_content(ss_content); all_hymn_numbers.extend(hymn_numbers); } } // Batch query all hymns from both hymnals let hymn_numbers_vec: Vec = all_hymn_numbers.into_iter().collect(); let hymnal_codes = vec!["sda-1985".to_string(), "sda-1941".to_string()]; let hymn_map = crate::sql::hymnal::get_hymns_by_numbers_batch(pool, &hymnal_codes, &hymn_numbers_vec).await?; // Now process each bulletin using the cached hymn data for bulletin in bulletins.iter_mut() { process_single_bulletin_with_cache(pool, bulletin, &hymn_map).await?; } Ok(()) } /// Process a single bulletin with all required transformations (optimized to prevent N+1) 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?; // Collect all hymn numbers from this bulletin first for batch lookup let mut all_hymn_numbers = std::collections::HashSet::new(); // Extract hymn numbers from divine_worship content if let Some(ref worship_content) = bulletin.divine_worship { let hymn_numbers = extract_hymn_numbers_from_content(worship_content); all_hymn_numbers.extend(hymn_numbers); } // Extract hymn numbers from sabbath_school content if let Some(ref ss_content) = bulletin.sabbath_school { let hymn_numbers = extract_hymn_numbers_from_content(ss_content); all_hymn_numbers.extend(hymn_numbers); } // Batch query all hymns from both hymnals for this single bulletin let hymn_numbers_vec: Vec = all_hymn_numbers.into_iter().collect(); let hymnal_codes = vec!["sda-1985".to_string(), "sda-1941".to_string()]; let hymn_map = crate::sql::hymnal::get_hymns_by_numbers_batch(pool, &hymnal_codes, &hymn_numbers_vec).await?; // Process hymn references using cached data if let Some(ref worship_content) = bulletin.divine_worship { bulletin.divine_worship = Some(process_hymn_references_with_cache(worship_content, &hymn_map)); } if let Some(ref ss_content) = bulletin.sabbath_school { bulletin.sabbath_school = Some(process_hymn_references_with_cache(ss_content, &hymn_map)); } // 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 direct SQL search // Allow up to 10 verses for ranges like "Matt 1:21-23" let verses = sqlx::query_as!( crate::models::BibleVerse, "SELECT * FROM bible_verses WHERE is_active = true AND (reference ILIKE $1 OR text ILIKE $1) ORDER BY reference LIMIT $2", format!("%{}%", scripture_text), 10i64 ) .fetch_all(pool) .await?; 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))) } } else { // 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) } /// Extract hymn numbers from content without making database calls fn extract_hymn_numbers_from_content(content: &str) -> Vec { let hymn_patterns = vec![ Regex::new(r"#(\d{1,3})").unwrap(), Regex::new(r"(?i)hymn\s+(\d{1,3})").unwrap(), Regex::new(r"(?i)no\.\s*(\d{1,3})").unwrap(), Regex::new(r"(?i)number\s+(\d{1,3})").unwrap(), ]; let mut hymn_numbers = Vec::new(); for pattern in hymn_patterns { for capture in pattern.captures_iter(content) { if let Some(number_match) = capture.get(1) { if let Ok(hymn_number) = number_match.as_str().parse::() { hymn_numbers.push(hymn_number); } } } } hymn_numbers } /// Process single bulletin using cached hymn data (no database calls for hymns) async fn process_single_bulletin_with_cache( pool: &sqlx::PgPool, bulletin: &mut Bulletin, hymn_cache: &std::collections::HashMap<(String, i32), crate::models::HymnWithHymnal> ) -> Result<()> { // Process scripture reading to include full verse text (still needs DB for bible verses) bulletin.scripture_reading = process_scripture_reading(pool, &bulletin.scripture_reading).await?; // Process hymn references in worship content using cache if let Some(ref worship_content) = bulletin.divine_worship { bulletin.divine_worship = Some(process_hymn_references_with_cache(worship_content, hymn_cache)); } // Process hymn references in sabbath school content using cache if let Some(ref ss_content) = bulletin.sabbath_school { bulletin.sabbath_school = Some(process_hymn_references_with_cache(ss_content, hymn_cache)); } // Ensure sunset field compatibility if bulletin.sunset.is_none() { bulletin.sunset = Some("TBA".to_string()); } Ok(()) } /// Process hymn references using cached hymn data (no database calls) fn process_hymn_references_with_cache( content: &str, hymn_cache: &std::collections::HashMap<(String, i32), crate::models::HymnWithHymnal> ) -> String { let hymn_patterns = vec![ Regex::new(r"#(\d{1,3})").unwrap(), Regex::new(r"(?i)hymn\s+(\d{1,3})").unwrap(), Regex::new(r"(?i)no\.\s*(\d{1,3})").unwrap(), Regex::new(r"(?i)number\s+(\d{1,3})").unwrap(), ]; let mut result = content.to_string(); // 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::() { // Look for hymn in 1985 hymnal first, then 1941 let hymn_title = hymn_cache.get(&("sda-1985".to_string(), hymn_number)) .or_else(|| hymn_cache.get(&("sda-1941".to_string(), hymn_number))) .map(|hymn| &hymn.title); 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); } } result }