use axum::{extract::State, Json}; use serde_json::{json, Value}; use crate::{ app::AppState, error::AppResult, services::{club as club_svc, objective as obj_svc, profile as profile_svc}, }; /// Returns actionable notifications for the current profile. /// /// Built dynamically from DB state — no separate notifications table. /// Covers: /// - Completed objectives whose reward hasn't been claimed yet /// - Loan cards that will expire within 2 matches /// - Season ending soon (≤2 matches remaining) pub async fn get_notifications(State(state): State) -> AppResult> { let profile = profile_svc::get_active_profile(&state.pool).await?; let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?; let mut notifications: Vec = Vec::new(); // 1. Completed but unclaimed objectives let objectives = obj_svc::get_objectives_with_progress(&state.pool, &profile.id, &state.obj_defs).await?; for obj in &objectives { if obj.completed && !obj.claimed { notifications.push(json!({ "type": "objective_complete", "objective_id": obj.definition.id, "title": format!("Objective complete: {}", obj.definition.title), "message": format!("Claim your reward for \"{}\"", obj.definition.title), "reward_coins": obj.definition.reward_coins, "reward_pack": obj.definition.reward_pack_id, })); } } // 2. Loan cards expiring within 2 matches let expiring_loans: Vec<(String, String, Option)> = sqlx::query_as( "SELECT oc.id, oc.card_id, oc.loan_matches_remaining \ FROM owned_cards oc WHERE oc.club_id = ? AND oc.is_loan = 1 \ AND oc.loan_matches_remaining <= 2", ) .bind(&club.id) .fetch_all(&state.pool) .await?; for (owned_id, card_id, remaining) in expiring_loans { let card_name = state .card_db .get(&card_id) .map(|c| c.name.clone()) .unwrap_or_else(|| card_id.clone()); notifications.push(json!({ "type": "loan_expiring", "owned_card_id": owned_id, "card_id": card_id, "title": format!("Loan expiring: {card_name}"), "message": format!( "{card_name} has {} match(es) remaining on loan", remaining.unwrap_or(0) ), "matches_remaining": remaining, })); } // 3. Season ending soon (≤2 matches remaining) let season_row: Option<(i64, i64)> = sqlx::query_as( "SELECT matches_played, division FROM seasons WHERE profile_id = ?", ) .bind(&profile.id) .fetch_optional(&state.pool) .await?; if let Some((played, division)) = season_row { let remaining = (10 - played).max(0); if remaining <= 2 && remaining > 0 { notifications.push(json!({ "type": "season_ending", "title": format!("Season ending — Division {division}"), "message": format!("{remaining} match(es) left in the season"), "matches_remaining": remaining, })); } } Ok(Json(json!({ "notifications": notifications, "count": notifications.len(), }))) }