use crate::{db::Pool, error::AppResult, services::{club, pack}}; use uuid::Uuid; const STREAK_COINS: [i64; 7] = [500, 650, 800, 1000, 1200, 1500, 2000]; // Day 7 (index 6) also grants a pack: const STREAK_7_PACK: &str = "silver_pack"; #[derive(Debug, serde::Serialize)] pub struct CheckinStatus { pub available: bool, pub streak_day: i64, // current streak (1–7 cycle, 0 if never checked in) pub next_reward_coins: i64, pub next_reward_pack: Option<&'static str>, pub last_checked_in: Option, } #[derive(Debug, serde::Serialize)] pub struct CheckinResult { pub coins_awarded: i64, pub pack_awarded: Option, pub new_streak: i64, pub already_claimed: bool, } pub async fn get_status(pool: &Pool, profile_id: &str) -> AppResult { let row: Option<(i64, String)> = sqlx::query_as( "SELECT streak_day, checked_in_at FROM daily_checkins \ WHERE profile_id = ? ORDER BY checked_in_at DESC LIMIT 1", ) .bind(profile_id) .fetch_optional(pool) .await?; let today = today_date(); match row { None => Ok(CheckinStatus { available: true, streak_day: 0, next_reward_coins: STREAK_COINS[0], next_reward_pack: None, last_checked_in: None, }), Some((last_streak, last_at)) => { let last_day = &last_at[..10]; // YYYY-MM-DD let available = last_day != today.as_str(); let next_streak = compute_next_streak(last_streak, &last_at); let idx = ((next_streak - 1) % 7) as usize; Ok(CheckinStatus { available, streak_day: if available { next_streak } else { last_streak }, next_reward_coins: STREAK_COINS[idx], next_reward_pack: if idx == 6 { Some(STREAK_7_PACK) } else { None }, last_checked_in: Some(last_at), }) } } } pub async fn claim( pool: &Pool, profile_id: &str, club_id: &str, ) -> AppResult { let row: Option<(i64, String)> = sqlx::query_as( "SELECT streak_day, checked_in_at FROM daily_checkins \ WHERE profile_id = ? ORDER BY checked_in_at DESC LIMIT 1", ) .bind(profile_id) .fetch_optional(pool) .await?; let today = today_date(); if let Some((_, ref last_at)) = row { if &last_at[..10] == today.as_str() { let last_streak = row.as_ref().map(|(s, _)| *s).unwrap_or(1); return Ok(CheckinResult { coins_awarded: 0, pack_awarded: None, new_streak: last_streak, already_claimed: true, }); } } let last_streak = row.as_ref().map(|(s, last_at)| compute_next_streak(*s, last_at)).unwrap_or(1); let idx = ((last_streak - 1) % 7) as usize; let coins = STREAK_COINS[idx]; let pack_def = if idx == 6 { Some(STREAK_7_PACK) } else { None }; club::add_coins(pool, club_id, coins).await?; if let Some(def) = pack_def { let _ = pack::grant_pack(pool, club_id, def).await; } let now = chrono::Utc::now().to_rfc3339(); sqlx::query( "INSERT INTO daily_checkins (id, profile_id, club_id, streak_day, coins_awarded, pack_granted, checked_in_at) \ VALUES (?, ?, ?, ?, ?, ?, ?)", ) .bind(Uuid::new_v4().to_string()) .bind(profile_id) .bind(club_id) .bind(last_streak) .bind(coins) .bind(pack_def) .bind(&now) .execute(pool) .await?; Ok(CheckinResult { coins_awarded: coins, pack_awarded: pack_def.map(String::from), new_streak: last_streak, already_claimed: false, }) } fn today_date() -> String { chrono::Utc::now().format("%Y-%m-%d").to_string() } /// If last check-in was yesterday or today → continue streak; otherwise reset to 1. fn compute_next_streak(last_streak: i64, last_at: &str) -> i64 { let last_day = &last_at[..10]; let today = chrono::Utc::now().date_naive(); let yesterday = (today - chrono::Days::new(1)).to_string(); let today_str = today.to_string(); if last_day == yesterday || last_day == today_str { (last_streak % 7) + 1 } else { 1 } }