use crate::{ db::Pool, error::{AppError, AppResult}, models::event::{EventDefinition, EventWithStatus}, }; use anyhow::Context; use std::{collections::HashMap, path::Path}; pub fn load_event_definitions(data_dir: &str) -> anyhow::Result> { let dir = Path::new(data_dir).join("events"); let mut defs = Vec::new(); if !dir.exists() { return Ok(defs); } for entry in std::fs::read_dir(&dir)? { let entry = entry?; let path = entry.path(); if path.extension().map(|e| e == "json").unwrap_or(false) { let content = std::fs::read_to_string(&path).with_context(|| format!("reading {:?}", path))?; let batch: Vec = serde_json::from_str(&content).with_context(|| format!("parsing {:?}", path))?; defs.extend(batch); } } Ok(defs) } fn compute_is_active(def: &EventDefinition, override_val: Option) -> bool { match override_val { Some(1) => return true, Some(0) => return false, _ => {} } if def.is_manual { return false; } let now = chrono::Utc::now(); let after_start = def .starts_at .as_ref() .and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok()) .is_none_or(|dt| now >= dt); let before_end = def .expires_at .as_ref() .and_then(|e| chrono::DateTime::parse_from_rfc3339(e).ok()) .is_none_or(|dt| now <= dt); after_start && before_end } async fn fetch_overrides(pool: &Pool) -> AppResult>> { let rows: Vec<(String, Option)> = sqlx::query_as("SELECT id, is_active_override FROM events") .fetch_all(pool) .await?; Ok(rows.into_iter().collect()) } pub async fn is_event_active(pool: &Pool, def: &EventDefinition) -> AppResult { let row: Option<(Option,)> = sqlx::query_as("SELECT is_active_override FROM events WHERE id = ?") .bind(&def.id) .fetch_optional(pool) .await?; let override_val = row.and_then(|(v,)| v); Ok(compute_is_active(def, override_val)) } pub async fn get_active_events( pool: &Pool, defs: &[EventDefinition], ) -> AppResult> { let overrides = fetch_overrides(pool).await?; Ok(defs .iter() .filter(|def| { let ov = overrides.get(&def.id).copied().flatten(); compute_is_active(def, ov) }) .cloned() .collect()) } type EventRow = (String, Option, Option, Option); type EventOverrideMap = HashMap, Option, Option)>; pub async fn get_all_with_status( pool: &Pool, defs: &[EventDefinition], ) -> AppResult> { let rows: Vec = sqlx::query_as("SELECT id, is_active_override, activated_at, deactivated_at FROM events") .fetch_all(pool) .await?; let db_map: EventOverrideMap = rows .into_iter() .map(|(id, ov, at, dt)| (id, (ov, at, dt))) .collect(); let result = defs .iter() .map(|def| { let (override_val, activated_at, deactivated_at) = db_map .get(&def.id) .map(|(ov, at, dt)| (*ov, at.clone(), dt.clone())) .unwrap_or((None, None, None)); EventWithStatus { is_active: compute_is_active(def, override_val), definition: def.clone(), activated_at, deactivated_at, } }) .collect(); Ok(result) } pub async fn activate_event( pool: &Pool, event_id: &str, defs: &[EventDefinition], ) -> AppResult<()> { defs.iter() .find(|d| d.id == event_id) .ok_or_else(|| AppError::NotFound(format!("event '{event_id}' not found")))?; let now = chrono::Utc::now().to_rfc3339(); sqlx::query( "INSERT INTO events (id, is_active_override, activated_at, deactivated_at) VALUES (?, 1, ?, NULL) ON CONFLICT(id) DO UPDATE SET is_active_override = 1, activated_at = excluded.activated_at, deactivated_at = NULL", ) .bind(event_id) .bind(&now) .execute(pool) .await?; Ok(()) } pub async fn deactivate_event( pool: &Pool, event_id: &str, defs: &[EventDefinition], ) -> AppResult<()> { defs.iter() .find(|d| d.id == event_id) .ok_or_else(|| AppError::NotFound(format!("event '{event_id}' not found")))?; let now = chrono::Utc::now().to_rfc3339(); sqlx::query( "INSERT INTO events (id, is_active_override, activated_at, deactivated_at) VALUES (?, 0, NULL, ?) ON CONFLICT(id) DO UPDATE SET is_active_override = 0, deactivated_at = excluded.deactivated_at, activated_at = NULL", ) .bind(event_id) .bind(&now) .execute(pool) .await?; Ok(()) }