Phase 11: FUT Champions mode and Division Rivals weekly rewards
CI / Build, lint & test (push) Failing after 1m33s

FUT Champions (Weekend League):
- POST /fut-champs/start — open a new 30-match week (one active session at a time)
- GET /fut-champs — current session status with matches_remaining
- POST /fut-champs/:id/result — record a match; auto-completes and computes tier at 30
- POST /fut-champs/:id/claim — claim coins + pack reward (idempotent guard)
- GET /fut-champs/history — past sessions newest-first
- 11 reward tiers: Elite (27+ wins, 50k coins + icon pack) down to Bronze 1 (0 wins, 250 coins)

Division Rivals:
- POST /rivals/claim-weekly — claim weekly reward scaled by current division
- Week counter is monotonic (offline-safe, no real-time calendar dependency)
- Division 1-3 get a bonus pack alongside coins

Migration 0008 adds fut_champs_sessions table and two columns to seasons.
12 new integration tests — all 67 pass, clippy clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-06-25 17:06:18 -07:00
parent 19c7b9989d
commit 26b8e9efef
9 changed files with 802 additions and 0 deletions
+108
View File
@@ -0,0 +1,108 @@
use serde::{Deserialize, Serialize};
pub const MAX_MATCHES: i64 = 30;
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct FutChampsSession {
pub id: String,
pub profile_id: String,
pub week_number: i64,
pub matches_played: i64,
pub wins: i64,
pub draws: i64,
pub losses: i64,
pub status: String,
pub reward_tier: Option<String>,
pub reward_coins: Option<i64>,
pub reward_pack_id: Option<String>,
pub reward_claimed: bool,
pub started_at: String,
pub completed_at: Option<String>,
}
impl FutChampsSession {
pub fn matches_remaining(&self) -> i64 {
(MAX_MATCHES - self.matches_played).max(0)
}
pub fn is_complete(&self) -> bool {
self.status == "completed"
}
}
/// Reward tier based on wins out of 30 matches — mirrors FUT 23 structure.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum ChampsTier {
Elite,
WorldClass,
Gold3,
Gold2,
Gold1,
Silver3,
Silver2,
Silver1,
Bronze3,
Bronze2,
Bronze1,
}
impl ChampsTier {
pub fn from_wins(wins: i64) -> Self {
match wins {
27.. => Self::Elite,
23..=26 => Self::WorldClass,
20..=22 => Self::Gold3,
17..=19 => Self::Gold2,
14..=16 => Self::Gold1,
11..=13 => Self::Silver3,
8..=10 => Self::Silver2,
5..=7 => Self::Silver1,
2..=4 => Self::Bronze3,
1 => Self::Bronze2,
_ => Self::Bronze1,
}
}
pub fn label(&self) -> &'static str {
match self {
Self::Elite => "Elite",
Self::WorldClass => "World Class",
Self::Gold3 => "Gold 3",
Self::Gold2 => "Gold 2",
Self::Gold1 => "Gold 1",
Self::Silver3 => "Silver 3",
Self::Silver2 => "Silver 2",
Self::Silver1 => "Silver 1",
Self::Bronze3 => "Bronze 3",
Self::Bronze2 => "Bronze 2",
Self::Bronze1 => "Bronze 1",
}
}
pub fn reward_coins(&self) -> i64 {
match self {
Self::Elite => 50_000,
Self::WorldClass => 30_000,
Self::Gold3 => 15_000,
Self::Gold2 => 10_000,
Self::Gold1 => 7_500,
Self::Silver3 => 5_000,
Self::Silver2 => 3_500,
Self::Silver1 => 2_000,
Self::Bronze3 => 1_000,
Self::Bronze2 => 500,
Self::Bronze1 => 250,
}
}
pub fn reward_pack(&self) -> Option<&'static str> {
match self {
Self::Elite => Some("icon_pack"),
Self::WorldClass => Some("rare_gold_pack"),
Self::Gold3 | Self::Gold2 | Self::Gold1 => Some("gold_pack"),
Self::Silver3 | Self::Silver2 | Self::Silver1 => Some("silver_pack"),
Self::Bronze3 => Some("bronze_pack"),
Self::Bronze2 | Self::Bronze1 => None,
}
}
}