Phase 24: daily check-in system + club milestones endpoint
CI / Build, lint & test (push) Failing after 28s

- migrations/0012_daily_checkin.sql: daily_checkins table tracking streak,
  coins awarded, pack granted, timestamp per profile
- services/checkin.rs: get_status() (available, streak_day, next reward),
  claim() (idempotent same-day guard, streak logic: continue if yesterday
  or today, else reset; 7-day cycle with STREAK_COINS array, day-7 pack)
- routes/club.rs: GET /club/checkin, POST /club/checkin, GET /club/milestones
  (computed from statistics, season_history, owned_cards, sbc_submissions,
  daily_checkins tables; no new DB tables needed)
- 4 new integration tests: checkin available initially, claim awards coins,
  idempotent same-day, milestones endpoint structure (93 → 93+4=97 tests)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-06-25 19:05:52 -07:00
parent 28d7490555
commit 956bfe7a73
6 changed files with 297 additions and 1 deletions
+61
View File
@@ -1789,3 +1789,64 @@ async fn test_division_response_has_zone_fields() {
assert_eq!(json["relegation_pts"], 3);
assert_eq!(json["season_length"], 10);
}
#[tokio::test]
async fn test_checkin_available_initially() {
let app = build_test_app().await;
auth(&app, "CheckinFirst").await;
let (status, json) = json_get(&app, "/club/checkin").await;
assert_eq!(status, StatusCode::OK, "{json}");
assert_eq!(json["available"], true);
assert_eq!(json["streak_day"], 0);
assert!(json["next_reward_coins"].is_number());
}
#[tokio::test]
async fn test_checkin_claim_awards_coins() {
let app = build_test_app().await;
auth(&app, "CheckinClaim").await;
let (_, before) = json_get(&app, "/club").await;
let coins_before = before["coins"].as_i64().unwrap_or(0);
let (status, json) = json_post(&app, "/club/checkin", serde_json::json!({})).await;
assert_eq!(status, StatusCode::OK, "{json}");
assert_eq!(json["already_claimed"], false);
assert!(json["coins_awarded"].as_i64().unwrap_or(0) > 0);
assert_eq!(json["new_streak"], 1);
let (_, after) = json_get(&app, "/club").await;
let coins_after = after["coins"].as_i64().unwrap_or(0);
assert!(coins_after > coins_before, "coins should increase after checkin");
}
#[tokio::test]
async fn test_checkin_idempotent_same_day() {
let app = build_test_app().await;
auth(&app, "CheckinIdempotent").await;
let (_, r1) = json_post(&app, "/club/checkin", serde_json::json!({})).await;
assert_eq!(r1["already_claimed"], false);
let (_, r2) = json_post(&app, "/club/checkin", serde_json::json!({})).await;
assert_eq!(r2["already_claimed"], true);
assert_eq!(r2["coins_awarded"], 0);
}
#[tokio::test]
async fn test_milestones_endpoint_structure() {
let app = build_test_app().await;
auth(&app, "MilestonePlayer").await;
let (status, json) = json_get(&app, "/club/milestones").await;
assert_eq!(status, StatusCode::OK, "{json}");
assert!(json["total_wins"].is_number());
assert!(json["total_goals_scored"].is_number());
assert!(json["best_win_streak"].is_number());
assert!(json["seasons_completed"].is_number());
assert!(json["highest_division_reached"].is_number());
assert!(json["cards_owned"].is_number());
assert!(json["sbcs_completed"].is_number());
assert!(json["total_checkins"].is_number());
}