use crate::{ db::Pool, error::AppResult, models::objective::{ObjectiveDefinition, ObjectiveProgress, ObjectiveWithProgress}, }; use anyhow::Context; use std::path::Path; use uuid::Uuid; pub fn load_objective_definitions(data_dir: &str) -> anyhow::Result> { let dir = Path::new(data_dir).join("objectives"); 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) } pub async fn get_objectives_with_progress( pool: &Pool, profile_id: &str, defs: &[ObjectiveDefinition], ) -> AppResult> { let progress_rows = sqlx::query_as::<_, ObjectiveProgress>( "SELECT id, profile_id, objective_id, current, completed, claimed, updated_at FROM objective_progress WHERE profile_id = ?" ) .bind(profile_id) .fetch_all(pool) .await?; let result = defs .iter() .map(|def| { let prog = progress_rows.iter().find(|p| p.objective_id == def.id); ObjectiveWithProgress { definition: def.clone(), current: prog.map(|p| p.current).unwrap_or(0), completed: prog.map(|p| p.completed).unwrap_or(false), claimed: prog.map(|p| p.claimed).unwrap_or(false), } }) .collect(); Ok(result) } /// Increment a metric for all objectives that track it. pub async fn increment_metric( pool: &Pool, profile_id: &str, defs: &[ObjectiveDefinition], metric: &str, amount: i64, ) -> AppResult> { let mut completed_ids = Vec::new(); for def in defs .iter() .filter(|d| format!("{:?}", d.metric).to_lowercase() == metric) { let existing = sqlx::query_as::<_, ObjectiveProgress>( "SELECT id, profile_id, objective_id, current, completed, claimed, updated_at FROM objective_progress WHERE profile_id = ? AND objective_id = ?" ) .bind(profile_id) .bind(&def.id) .fetch_optional(pool) .await?; let now = chrono::Utc::now().to_rfc3339(); if let Some(prog) = existing { if prog.completed { continue; } let new_val = (prog.current + amount).min(def.target); let now_complete = new_val >= def.target; sqlx::query( "UPDATE objective_progress SET current = ?, completed = ?, updated_at = ? WHERE id = ?" ) .bind(new_val) .bind(now_complete) .bind(&now) .bind(&prog.id) .execute(pool) .await?; if now_complete { completed_ids.push(def.id.clone()); } } else { let new_val = amount.min(def.target); let now_complete = new_val >= def.target; let id = Uuid::new_v4().to_string(); sqlx::query( "INSERT INTO objective_progress (id, profile_id, objective_id, current, completed, claimed, updated_at) VALUES (?, ?, ?, ?, ?, 0, ?)" ) .bind(&id) .bind(profile_id) .bind(&def.id) .bind(new_val) .bind(now_complete) .bind(&now) .execute(pool) .await?; if now_complete { completed_ids.push(def.id.clone()); } } } Ok(completed_ids) }