Correctness fixes from docs/CORE_CORRECTNESS_ISSUES.md:
- Issue 3 (HIGH, exploit): submit_sbc dedups owned_card_ids (HashSet) and
bounds the list (MAX_SBC_CARDS=30) before resolution. A repeated id resolved
the same card N times, passed validation, and granted the reward while only
one card was consumed -> any SBC satisfiable with one duplicated card = free
reward. Now rejected with BadRequest. Regression test added.
- Issue 2 (HIGH): TOCTOU economy mutations closed with single-statement
compare-and-swap (no transaction plumbing): club::spend_coins conditional
debit (WHERE coins >= ?) + rows_affected, also rejects negative amounts;
pack::open_pack claims the pack before minting; market::buy_listing claims
the listing before charging and releases on debit failure; market::sell_card
guards the DELETE with owner + rows_affected; checkin::claim uses a
conditional INSERT ... WHERE NOT EXISTS (today) before paying out.
- Issue 4 (LOW): season.rs .expect() on missing rows -> graceful AppError;
checkin index (streak-1) % 7 -> .rem_euclid(7) (guards negative index panic).
- Issue 1 (LOW): migration 0019 adds sbc_submissions.club_id + backfill;
submit_sbc binds it so the MY CLUB milestone query stops silently reading 0.
Core suite 179 green + clippy clean.
- GET /division returns live season stats (points, record, promotion threshold)
- PUT /club allows updating club name and manager_name
- GET /packs/history returns opened packs with full card definitions
- GET /notifications dynamically surfaces completed objectives, expiring loans, season end
- Club model gains manager_name column (migration 0006 already added it)
- Pack model gains opened_cards and opened_at; pack SELECT queries updated
- 9 new integration tests — all 45 pass, clippy clean
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>