Closes 4 more arena-colosseum spec shapes (6/16 → 10/16): the dual-prefix
battle URLs (colosseum_battle/* and colosseum_rank_battle/*) for both
do_matching and per-match finish, plus the bracket-end /finish + /retire
pair on /arena_colosseum.
* ColosseumProgressionService: pure-logic decisions for win-threshold
advancement, 3008 promotion trigger, and per-round reward bundles.
Extends ColosseumRoundsConfig with FinishRewards/RetireRewards per
round + a top-level ChampionRewards list.
* MatchContextBuilder.BuildForColosseumAsync: lifts the registered deck
via IDeckRepository (NOT viewer-graph) per project_ef_nav_include_pitfall.
* ArenaColosseumBattleController: dual route prefixes off one controller
via explicit absolute [HttpPost("/...")] routes — does NOT extend
SVSimController's [Route("[controller]")]. Same pattern as FreeBattle.
URL-vs-IsRankMatching mismatch returns 400 with a phase-mismatch error;
no-deck triggers the standard 3001 matchmaking-illegal state. 3008
flips run.IsRankMatching exactly once via the progression service.
* /arena_colosseum/finish + /retire share FinishResponse (rewards +
reward_list + colosseum_status). Champion finish appends ChampionRewards
and surfaces colosseum_status.is_champion = true. Both delete the run
row after granting via IInventoryTransaction.GrantAsync.
* DI: ModePolicy entries for "colosseum_battle" and "colosseum_rank_battle".
* Tests: 8 progression-service unit tests (pure logic), 5 battle-controller
HTTP tests, 5 bracket-terminate HTTP tests, 2 full-bracket E2E walks
(champion path + retire mid-round). Full suite: 1331/1331.
Strict yyyy-MM-dd parse (matches /load/index round-trip format) — rejects
malformed input. data_headers.servertime is emitted by the standard
envelope, which is what the client reads into BirthDayUpdateServerTime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
is_skip_gacha_effect, use_challenge_two_pick_premium_card, and
challenge_two_pick_sleeve_id all persist on ViewerInfo and round-trip
through /load/index.
The two challenge fields move from the global ChallengeConfig section to
ViewerInfo — they're viewer preferences, not server-wide policy. Premium
card defaults to 0; sleeve falls back to DefaultLoadoutConfig.SleeveId
(3000011) when unset. MatchContextBuilder's TK2 sleeve read switches to
the same viewer-scoped path.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Translation middleware now extracts viewer_id/steam_id/steam_session_ticket
from the decrypted msgpack dict into HttpContext.Items before the typed
DTO deserialize. The Steam handler reads from there instead of re-parsing
Request.Body — so authed action DTOs no longer need to inherit BaseRequest
to keep the auth fields alive through the msgpack→DTO→JSON pivot.
Retires the recurring footgun documented in
docs/superpowers/specs/2026-06-02-baseRequest-auth-footgun-improvement.md
(2026-05-25 basic-puzzle, 2026-05-28 deck-code, 2026-06-02 Phase 3 Bot,
2026-06-10 profile/index + item_acquire_history/info + user_mypage/update).
Pinned by AuthDecouplingTests — posts an encrypted msgpack body to
/profile/index (DTO does not inherit BaseRequest) through the real
translation middleware + auth handler and asserts 200. Adds an
EncryptedMsgpackHelper + useRealAuthHandler factory flag, reusable for
future wire-shape tests.
ProfileIndexRequest, ItemAcquireHistoryInfoRequest, and
UserMyPageUpdateRequest revert to the naked shape — the per-DTO
workarounds become vestigial under the new architecture.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per feedback_wire_shape_tests: controller round-trip tests using the
same DTO can't catch wire-key/wire-type drift. Asserts parsing of and
snake_case emission for the period list + monthly ranking shapes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pure deterministic monthly period generator for the four ranking
families. Anchor dates derived from prod capture (2026-06-09): id=1 is
each family's launch month in JST; id=N is anchor + N-1 months. Used
by /ranking/get_viewable_ranking_period_list to render the period
picker and by per-family leaderboard endpoints to echo the requested
period back.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ShadowverseTranslationMiddleware throws InvalidOperationException when
a Unity client posts an encrypted msgpack body to an action with zero
[FromBody] parameters — it has no target type for the deserializer.
Tests pass because they post JSON directly with no UnityPlayer UA and
the middleware short-circuits. Same defect already fixed on /replay/info
in 216dcab; this catches up the friend system shipped 2026-06-09.
Fixed actions: info, receive_apply_info, send_apply_info,
played_together_info, reject_apply_all, cancel_apply_all.
Tests updated to post the BaseRequest auth fields so [ApiController]
model validation passes (BaseRequest.ViewerId is non-nullable string).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per feedback_wire_shape_tests (2026-05-28): controller round-trip tests
using the same DTO can't catch wire-key/wire-type drift. Asserts
parsing of, snake_case emission for, and MessagePack round-trip
through the captured prod frame.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Same pattern as rank-battle: DoMatching stashes context; Finish takes
it and records history + played-together. Opponent identity is left
as placeholder fields until the resolver carries it through.
Test seeds an active ViewerArenaTwoPickRun so RecordBattleResultAsync
does not throw no_active_run during the e2e flow.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Finish now consumes the stashed BattleContext, records a
ViewerBattleHistory row (idempotent + retention-capped), and
calls IPlayedTogetherWriter for human PvP (skipped for AI).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Following the 216dcab fix that added [FromBody] BaseRequest _ to the
Info action, the existing tests' empty new {} payloads no longer satisfy
[ApiController] model validation (BaseRequest.ViewerId is non-nullable
string). Use the same EmptyBody() shape as RankBattleControllerTests
to mirror the production wire.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
/replay/info reads from ReplayHistoryReader, newest-first, capped at 50.
/replay/detail returns 400 with result_code=99 - local cache is the
canonical playback source so this endpoint is cache-miss fallback only.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Idempotent on (ViewerId, BattleId); evicts oldest CreateTime row when
at cap. No-op when ctx is null (server-restart safety).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bridges the start-time -> finish-time gap. /finish carries neither
battle_id nor opponent identity; this store holds both for the finish
handler to compose a ViewerBattleHistory row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
New table backs /replay/info; composite PK (ViewerId, BattleId), index on
(ViewerId, CreateTime) for the newest-first list query. 50-row per-viewer
retention enforced by BattleHistoryWriter (next commit).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>