Files
OpenFUT-Core/src/services/profile.rs
T
OpenFUT Agent f70cf4415c wip(core): preserve local broad Core refactor + inventory service
Divergent development line off origin/main (11a811d): a broad refactor across
routes/services/models/app + a large integration_test expansion (+1000), plus an
untracked game-independent inventory query service and Docker files. Preserved
verbatim before moving the canonical Core checkout to the committed migration
trunk (66c88fb). Reconciling this refactor with the migration trunk is a separate
user decision; nothing here is lost.
2026-08-13 18:34:00 +00:00

98 lines
2.9 KiB
Rust

use crate::{
db::Pool,
error::{AppError, AppResult},
models::profile::{coins_for_level, level_for_xp, pack_for_level, LevelUpEvent, Profile},
};
use chrono::Utc;
pub async fn get_active_profile(pool: &Pool) -> AppResult<Profile> {
sqlx::query_as::<_, Profile>(
"SELECT id, username, level, xp, created_at, updated_at FROM profiles ORDER BY created_at ASC LIMIT 1"
)
.fetch_optional(pool)
.await?
.ok_or_else(|| AppError::NotFound("no profile exists; call POST /auth/local first".into()))
}
pub async fn create_profile(pool: &Pool, username: &str) -> AppResult<Profile> {
let existing = sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM profiles")
.fetch_one(pool)
.await?;
if existing > 0 {
return Err(AppError::Conflict(
"a profile already exists; OpenFUT is single-player only".into(),
));
}
let profile = Profile::new(username);
sqlx::query(
"INSERT INTO profiles (id, username, level, xp, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)"
)
.bind(&profile.id)
.bind(&profile.username)
.bind(profile.level)
.bind(profile.xp)
.bind(profile.created_at)
.bind(profile.updated_at)
.execute(pool)
.await?;
Ok(profile)
}
pub async fn add_xp(pool: &Pool, profile_id: &str, xp: i64) -> AppResult<()> {
let now = Utc::now();
sqlx::query("UPDATE profiles SET xp = xp + ?, updated_at = ? WHERE id = ?")
.bind(xp)
.bind(now)
.bind(profile_id)
.execute(pool)
.await?;
Ok(())
}
/// Add XP, check for level-ups, grant rewards, and return every level gained.
/// Callers should use this instead of `add_xp` when level-up feedback matters.
pub async fn add_xp_with_levelup(
pool: &Pool,
profile_id: &str,
club_id: &str,
xp_to_add: i64,
) -> AppResult<Vec<LevelUpEvent>> {
let profile = get_active_profile(pool).await?;
let old_level = level_for_xp(profile.xp);
let new_total_xp = profile.xp + xp_to_add;
let new_level = level_for_xp(new_total_xp);
let now = Utc::now();
sqlx::query("UPDATE profiles SET xp = ?, level = ?, updated_at = ? WHERE id = ?")
.bind(new_total_xp)
.bind(new_level)
.bind(now)
.bind(profile_id)
.execute(pool)
.await?;
let mut events = Vec::new();
for lvl in (old_level + 1)..=new_level {
let coins = coins_for_level(lvl);
let pack = pack_for_level(lvl).map(String::from);
if coins > 0 {
crate::services::club::add_coins(pool, club_id, coins).await?;
}
if let Some(ref pack_id) = pack {
crate::services::pack::grant_pack(pool, club_id, pack_id).await?;
}
tracing::info!(profile_id, new_level = lvl, coins, "level up");
events.push(LevelUpEvent {
new_level: lvl,
coins_granted: coins,
pack_granted: pack,
});
}
Ok(events)
}