Phase 10: card upgrade system (chemistry styles, position change, training)
CI / Build, lint & test (push) Failing after 1m37s

- GET /chemistry-styles — lists 18 FUT-style chemistry styles with stat boost breakdowns
- POST /collection/:id/chemistry-style — apply a style (Shadow, Hunter, Anchor, etc.)
- POST /collection/:id/position — override a player's position for 500 coins
- POST /collection/:id/training — add +1/+2/+3 OVR training bonus (max +3 total)
- GET /collection now includes chemistry_style, position_override, training_bonus,
  effective_overall and effective_position fields per owned card
- Migration 0007 adds three columns to owned_cards with safe defaults
- 10 new integration tests — all 55 pass, clippy clean

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-06-25 17:02:44 -07:00
parent a749fba93c
commit 19c7b9989d
15 changed files with 617 additions and 6 deletions
+120
View File
@@ -0,0 +1,120 @@
use axum::{
extract::{Path, State},
Json,
};
use serde::Deserialize;
use serde_json::{json, Value};
use crate::{
app::AppState,
error::AppResult,
services::{club as club_svc, profile as profile_svc, upgrades as upgrade_svc},
};
/// GET /chemistry-styles — list all available styles with their stat boosts.
pub async fn get_chemistry_styles(State(state): State<AppState>) -> AppResult<Json<Value>> {
Ok(Json(json!({
"chemistry_styles": *state.chem_styles,
"total": state.chem_styles.len(),
})))
}
#[derive(Deserialize)]
pub struct ApplyChemStyleRequest {
pub style_id: String,
}
/// POST /collection/:owned_card_id/chemistry-style
pub async fn post_apply_chemistry_style(
State(state): State<AppState>,
Path(owned_card_id): Path<String>,
Json(req): Json<ApplyChemStyleRequest>,
) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
let updated = upgrade_svc::apply_chemistry_style(
&state.pool,
&club.id,
&owned_card_id,
&req.style_id,
&state.chem_styles,
)
.await?;
let style = state
.chem_styles
.iter()
.find(|s| s.id == updated.chemistry_style);
Ok(Json(json!({
"owned_card_id": updated.id,
"card_id": updated.card_id,
"chemistry_style": updated.chemistry_style,
"style_details": style,
})))
}
#[derive(Deserialize)]
pub struct ChangePositionRequest {
pub position: String,
}
/// POST /collection/:owned_card_id/position — costs 500 coins.
pub async fn post_change_position(
State(state): State<AppState>,
Path(owned_card_id): Path<String>,
Json(req): Json<ChangePositionRequest>,
) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
let updated = upgrade_svc::change_position(
&state.pool,
&club.id,
&owned_card_id,
&req.position,
)
.await?;
let card_def = state.card_db.get(&updated.card_id);
Ok(Json(json!({
"owned_card_id": updated.id,
"card_id": updated.card_id,
"original_position": card_def.map(|c| &c.position),
"position_override": updated.position_override,
"cost_coins": upgrade_svc::POSITION_CHANGE_COST,
})))
}
#[derive(Deserialize)]
pub struct ApplyTrainingRequest {
/// OVR points to add. Must be 13. Total capped at +3.
pub boost: i64,
}
/// POST /collection/:owned_card_id/training — applies a training boost (up to +3 OVR total).
pub async fn post_apply_training(
State(state): State<AppState>,
Path(owned_card_id): Path<String>,
Json(req): Json<ApplyTrainingRequest>,
) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
let updated =
upgrade_svc::apply_training(&state.pool, &club.id, &owned_card_id, req.boost).await?;
let card_def = state.card_db.get(&updated.card_id);
let base_overall = card_def.map(|c| c.overall as i64).unwrap_or(0);
Ok(Json(json!({
"owned_card_id": updated.id,
"card_id": updated.card_id,
"base_overall": base_overall,
"training_bonus": updated.training_bonus,
"effective_overall": base_overall + updated.training_bonus,
"max_bonus": upgrade_svc::MAX_TRAINING_BONUS,
})))
}