Initial commit: OpenFUT Core

Offline Ultimate Team backend — game-independent REST API.

- 19 API endpoints: auth, profiles, clubs, cards, packs, squads,
  objectives, SBCs, match rewards, NPC market, statistics
- Axum + SQLite + SQLx with full migrations
- Weighted pack generator, SBC validation engine
- JSON-driven mod data (cards, packs, objectives, SBCs)
- 5 integration tests passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-06-25 14:52:06 -07:00
commit 1ffe0ffa9f
60 changed files with 6152 additions and 0 deletions
+71
View File
@@ -0,0 +1,71 @@
use crate::{
db::Pool,
error::{AppError, AppResult},
models::club::Club,
};
use chrono::Utc;
pub async fn get_club_by_profile(pool: &Pool, profile_id: &str) -> AppResult<Club> {
sqlx::query_as::<_, Club>(
"SELECT id, profile_id, name, coins, level, created_at, updated_at FROM clubs WHERE profile_id = ? LIMIT 1"
)
.bind(profile_id)
.fetch_optional(pool)
.await?
.ok_or_else(|| AppError::NotFound("club not found".into()))
}
pub async fn create_club(pool: &Pool, club: &Club) -> AppResult<()> {
sqlx::query(
"INSERT INTO clubs (id, profile_id, name, coins, level, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
)
.bind(&club.id)
.bind(&club.profile_id)
.bind(&club.name)
.bind(club.coins)
.bind(club.level)
.bind(club.created_at)
.bind(club.updated_at)
.execute(pool)
.await?;
Ok(())
}
pub async fn add_coins(pool: &Pool, club_id: &str, amount: i64) -> AppResult<i64> {
let now = Utc::now();
sqlx::query("UPDATE clubs SET coins = coins + ?, updated_at = ? WHERE id = ?")
.bind(amount)
.bind(now)
.bind(club_id)
.execute(pool)
.await?;
let new_balance = sqlx::query_scalar::<_, i64>("SELECT coins FROM clubs WHERE id = ?")
.bind(club_id)
.fetch_one(pool)
.await?;
Ok(new_balance)
}
pub async fn spend_coins(pool: &Pool, club_id: &str, amount: i64) -> AppResult<i64> {
let balance = sqlx::query_scalar::<_, i64>("SELECT coins FROM clubs WHERE id = ?")
.bind(club_id)
.fetch_one(pool)
.await?;
if balance < amount {
return Err(AppError::BadRequest(format!(
"insufficient coins: have {balance}, need {amount}"
)));
}
let now = Utc::now();
sqlx::query("UPDATE clubs SET coins = coins - ?, updated_at = ? WHERE id = ?")
.bind(amount)
.bind(now)
.bind(club_id)
.execute(pool)
.await?;
Ok(balance - amount)
}