feat(club): persist active home/away kit assignments
CI / Build, lint & test (push) Successful in 2m50s

Kits are ownership-backed club items: the owned instance stays in the
generic owned_cards inventory and only the two active roles get their own
table. This mirrors the squad_managers precedent and keeps every
FIFA-specific resourceId/wire concern in the game adapter.

* migration 0024: club_kit_assignments(club_id, slot, owned_card_id) with a
  UNIQUE owned_card_id (one instance cannot hold both roles) and
  ON DELETE CASCADE from owned_cards so a quick-sell clears the role.
* a BEFORE UPDATE OF club_id trigger clears the designation on a market
  transfer, which moves ownership by UPDATE and so is not covered by the
  cascade.
* set_active_club_kits replaces BOTH slots in one transaction, rejects
  home == away, and validates each instance against current club ownership,
  so a half-applied or dangling designation is not representable.
* get_active_club_kits revalidates ownership on read, so a stale row can
  never surface another club's item.
* GET/PUT /club/kits expose the pair.

Tests cover restart persistence, replace/clear without duplicates, atomic
rejection of invalid references, and clearing via delete and transfer.
This commit is contained in:
funman300
2026-08-21 03:17:40 +00:00
parent 2fb835200f
commit f0550e2ae1
4 changed files with 259 additions and 8 deletions
+37
View File
@@ -163,3 +163,40 @@ pub async fn put_squad_manager(
let manager = club_svc::get_squad_manager(&state.pool, &club.id).await?;
Ok(Json(json!({ "manager": manager })))
}
/// Return the club's ownership-backed active home/away kit assignments.
pub async fn get_active_kits(
State(state): State<AppState>,
game: GameId,
) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
let kits = club_svc::get_active_club_kits(&state.pool, &club.id).await?;
Ok(Json(json!({ "home": kits.home, "away": kits.away })))
}
#[derive(Deserialize)]
pub struct SetActiveKitsRequest {
pub home_owned_card_id: Option<String>,
pub away_owned_card_id: Option<String>,
}
/// Atomically replace both active kit assignments. Core enforces ownership and
/// distinct instances; game adapters enforce their own definition taxonomy.
pub async fn put_active_kits(
State(state): State<AppState>,
game: GameId,
Json(req): Json<SetActiveKitsRequest>,
) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
club_svc::set_active_club_kits(
&state.pool,
&club.id,
req.home_owned_card_id.as_deref(),
req.away_owned_card_id.as_deref(),
)
.await?;
let kits = club_svc::get_active_club_kits(&state.pool, &club.id).await?;
Ok(Json(json!({ "home": kits.home, "away": kits.away })))
}