Phase 8 (Core): stateful draft, quick-sell, objectives by ID, market listings
CI / Build, lint & test (push) Failing after 1m21s

Draft v2 — stateful FUT-style pick sessions:
  - POST /draft/start?difficulty=<> — creates session, returns 5
    candidates for GK slot (position order: GK RB CB CB LB CDM CM CAM RW ST LW)
  - POST /draft/sessions/:id/pick { card_id } — validates candidate,
    advances to next position; on last pick grants coins+pack reward
    (avg OVR ≥84 → gold pack + 2000 coins, ≥78 → silver + 1000, else 400)
  - GET /draft/sessions/:id — session state with per-pick cards
  - POST /draft/sessions/:id/abandon — cancel without reward
  - Migration 0005_draft_sessions.sql

Quick-sell:
  - DELETE /collection/:owned_card_id — removes card, credits coins based
    on overall (85+ → 1500, 80-84 → 900, 75-79 → 600, 65-74 → 300, <65 → 150)

Objectives:
  - GET /objectives/:id — single objective with progress
  - POST /objectives/:id/claim — claim reward by URL param (complement to
    existing POST /objectives/claim body-param endpoint)

Market:
  - GET /market/my-listings — active listings posted by current club
  - DELETE /market/listings/:id — cancel a listing, returns card to collection

Tests: 9 new integration tests (37 total, all passing)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-06-25 16:41:55 -07:00
parent 8fa125cfd6
commit bfd6de6896
12 changed files with 828 additions and 9 deletions
+71 -6
View File
@@ -1,27 +1,28 @@
use axum::{
extract::{Query, State},
extract::{Path, Query, State},
Json,
};
use serde::Deserialize;
use serde_json::{json, Value};
use crate::{app::AppState, error::AppResult, services::match_service};
use crate::{
app::AppState,
error::AppResult,
services::{club as club_svc, draft as draft_svc, match_service, profile as profile_svc},
};
#[derive(Deserialize)]
pub struct DraftQuery {
pub difficulty: Option<String>,
}
/// Returns a randomly generated draft squad from the card pool.
/// Difficulty controls minimum card overall. The front-end is responsible
/// for presenting per-position pick choices; this endpoint provides the pool.
/// Legacy endpoint: returns a pre-built random draft squad (no session state).
pub async fn get_draft_squad(
State(state): State<AppState>,
Query(query): Query<DraftQuery>,
) -> AppResult<Json<Value>> {
let difficulty = query.difficulty.as_deref().unwrap_or("beginner");
let generated = match_service::generate_opponent(&state.card_db, difficulty);
Ok(Json(json!({
"mode": "draft",
"difficulty": difficulty,
@@ -30,3 +31,67 @@ pub async fn get_draft_squad(
"cards": generated["cards"],
})))
}
/// Start a new stateful FUT-style draft session.
///
/// Returns a session ID and the 5 candidates for the first position (GK).
/// The caller picks one candidate, then calls `POST /draft/sessions/:id/pick`
/// until all 11 slots are filled.
pub async fn post_draft_start(
State(state): State<AppState>,
Query(query): Query<DraftQuery>,
) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
let difficulty = query.difficulty.as_deref().unwrap_or("professional");
let session = draft_svc::start_draft(&state.pool, &state.card_db, &profile.id, difficulty).await?;
Ok(Json(session))
}
/// Get the current state of a draft session.
pub async fn get_draft_session(
State(state): State<AppState>,
Path(session_id): Path<String>,
) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
let session = draft_svc::get_draft(&state.pool, &state.card_db, &profile.id, &session_id).await?;
Ok(Json(session))
}
#[derive(Deserialize)]
pub struct PickRequest {
pub card_id: String,
}
/// Pick one card from the current candidates list.
///
/// Advances the session to the next position.
/// When the last pick is made the session status changes to "completed"
/// and rewards (coins + optional pack) are granted automatically.
pub async fn post_draft_pick(
State(state): State<AppState>,
Path(session_id): Path<String>,
Json(req): Json<PickRequest>,
) -> 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 session = draft_svc::pick_card(
&state.pool,
&state.card_db,
&club.id,
&profile.id,
&session_id,
&req.card_id,
)
.await?;
Ok(Json(session))
}
/// Abandon an active draft session. No rewards are granted.
pub async fn post_draft_abandon(
State(state): State<AppState>,
Path(session_id): Path<String>,
) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
let result = draft_svc::abandon_draft(&state.pool, &profile.id, &session_id).await?;
Ok(Json(result))
}