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>
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>
AiStartInternal now writes a BattleContext keyed by viewer id; the next
commit consumes it in /finish to write a ViewerBattleHistory row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The translation middleware throws InvalidOperationException when an
action has no body parameters and a Unity client sends an encrypted
msgpack body. Tests pass without it because they post JSON directly
and bypass the middleware (no UnityPlayer User-Agent).
See RankBattleController.ForceFinish for the documented convention.
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>
All numeric fields on ReplayInfoItemDto ship as string — matches prod
capture (frame 96 of traffic_prod_misc_clicking.ndjson). api-spec doc
will be updated separately.
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>
GetFriendsAsync, GetReceiveAppliesAsync, GetSendAppliesAsync, GetPlayedTogetherAsync,
SearchAsync all implemented. LoadViewerProjectionAsync materialises the full Viewer
entity (with Include/ThenInclude for SelectedEmblem/Degree) then projects in-memory —
avoids the EF Core limitation where Include is silently ignored under Select.
FriendService + IPlayedTogetherWriter registered as Scoped in Program.cs.
12 read tests, all green; full suite 1171/1171 still passing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Delete local IsSupportedGiftRewardType and replace its single call site with
GiftRewardTypes.IsSupported — Card (5) and Sleeve (6) are now accepted.
Update unsupported-type test sentinel from 5 (Card) to 11 (SpotCard).
Add Card and Sleeve success-path tests; full suite 1152/1152.
Replace WireRewardTypeToUserGoodsType switch with a validating identity cast backed
by GiftRewardTypes.IsSupported. Wire type 1 is RedEther (UserGoodsType.RedEther),
not Crystal (UserGoodsType.Crystal=2); the old switch silently granted the wrong
wallet for every tutorial-completion claim. Update all 5 GiftControllerTests assertions
and 1 TutorialFlowEndToEndTests assertion to expect RedEther instead of Crystals.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Centralizes which UserGoodsType values IInventoryTransaction.GrantAsync
can handle in the gift-inbox flow; both GiftController (post-fix) and
CampaignController will consult this instead of duplicating the set.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds POST /campaign/regist_serial_code with 9 integration tests covering
success path, all disqualifier conditions (unknown/disabled/expired/future/
already-redeemed/unsupported-reward-type), 401 on missing auth, and case-
sensitivity. IsSupportedGiftRewardType uses gift-wire literals (1/4/9) not
UserGoodsType enum values, matching GiftController.WireRewardTypeToUserGoodsType.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add /profile/index endpoint that returns user_rank_match_total_win (stubbed 0)
and user_class_list built from viewer Classes + owned LeaderSkins. Six NUnit
integration tests cover zero wins, all classes present, level/exp/default skin,
leader_skin_id_list population, is_random_leader_skin round-trip, and 401 on
unauthenticated access.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wires MyPageController.Index to read Viewer.MyPageBgId/SelectType/BgRotation
instead of emitting an empty MyPageBgSetting placeholder; adds Include for
MyPageBgRotation in ViewerRepository.GetViewerByShortUdid; adds fresh-viewer
zero-defaults test and write→read round-trip test (9/9 controller tests pass).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a comment above MyPageBgRotation.Clear() explaining the EF OwnsMany
delete-on-clear semantics. Add a 7th test covering garbage/"" entries in
mypage_id_list falling back to BgId=0 while adjacent valid entries are
unaffected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prod capture (traffic_prod_misc_clicking.ndjson) shows all three
MyPageBgSetting fields arrive as decimal strings, not ints. Update the
DTO from int/int/List<int> to string/string/List<string> with "0"/empty
defaults, and add a literal wire-shape round-trip test pinning the
exact JSON against the capture.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduce InventoryHistoryConfig.RetentionRowsPerViewer as the single
source of truth for the 300-row audit-log cap; InventoryTransaction
aliases it and ItemAcquireHistoryController.Take() references it
directly so the two sites cannot drift. Also adds CultureInfo.InvariantCulture
to the AcquireTime.ToString() call, matching every other WireDateFormat
site in the codebase.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add ItemAcquireHistoryController (POST /item_acquire_history/info) with its
three DTOs and two integration tests (ordering + empty-viewer). The endpoint
reads ViewerAcquireHistory rows written by InventoryTransaction.CommitAsync,
ordered newest-first, capped at 300. Tests access doc.RootElement.histories
directly (no envelope wrapper in the test path — middleware skips non-UnityPlayer UA).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
[Timestamp] byte[] doesn't work under SQLite (the test backend) — EF
expects the DB to populate it on insert, but SQLite has no equivalent
of Postgres's xmin. The WHERE Status = Unclaimed filter plus
IInventoryService's viewer-level concurrency is the practical defense;
RowVersion was only a backstop. Regenerated the migration without the
RowVersion column.
Wire reward_type on the gift endpoint uses a gift-specific scheme that
diverges from UserGoodsType for currencies: wire 1 = Crystal (enum=2),
wire 9 = Rupy (enum=9), wire 4 = Item (enum=4). A naked cast resolves
wire 1 to UserGoodsType.RedEther and silently grants the wrong wallet
— restored the explicit WireRewardTypeToUserGoodsType map from the old
tutorial controller.
Retrofits existing GiftControllerTests to call SeedTutorialPresentsAsync
on the new helper (RegisterViewer doesn't auto-seed; only the prod
signup path does). All 7 existing tests pass.
Walk-down behavior: each call emits the highest-priority unfired
active dialog; subsequent calls walk to the next-priority entry.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Singleton keyed by ShortUdid; lock on per-viewer set to avoid
cross-viewer contention. Process lifetime — restart re-fires.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Behavior-preserving; full solution builds, 1013 tests green.
ClassId is the one genuinely-closed set of the three flagged stringly fields, so it
becomes a CardClass enum (1..8). Wire stays "1".."8": producer casts
(CardClass)run.ClassId, ServerBattleFrames renders via CardClassWire.ToWireValue().
RankBattleController's AI-start path drops a fragile int.TryParse(...)?:-1 for (int)cast.
CharaId (free-form leader/skin id, e.g. "5000123") and CountryCode (open-ended account
data) stay string with proper XML docs; CountryCodes.Korea/Japan name the captured values.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Behavior-preserving; 271 BattleNode/Matching/Services tests green, full solution builds.
"BattleType" meant two things: the Sessions.BattleType enum (Pvp/Bot) and an int
"mode id" field. Renamed the int field on MatchContext AND the BattleStartBody wire
DTO to BattleModeId (wire key stays "battleType" via JsonPropertyName), so BattleType
now means only the enum project-wide.
New Bridge/BattleModes.cs (TakeTwo = 11) replaces every 11 literal — both prod
MatchContextBuilder sites and the test fixtures/assertions. The arbitrary-passthrough
42 and bot 0 stay literal.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>