EnsureCurrentAsync now takes viewerId (was Viewer), so it works with
LoadController's AsNoTracking-loaded detached viewers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three endpoints + 9 integration tests. Captured-data-is-catalog: viewer's
achievement Level starts at MIN(Level) per type from the catalog (not 1),
so the assembler always has a row to render against.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tests intentionally deferred to controller integration tests (Tasks
18-21) which exercise the assembler end-to-end via the wire.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reads existing state from DB on each call (don't trust navigation
property — caller may pass it stale or double-tracked). Adds via DbSet
only, not via navigation property, to avoid EF double-tracking.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Also wires IMissionCatalogRepository + IViewerMissionRepository +
IMissionProgressService into DI. Task 17's separate DI step is now
subsumed by these registrations.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Crystal synthesis in BuyPremiumAsync is now unconditional: always remove
any crystal entry ApplyAsync may have added, then append the fresh
post-deduction total. Prevents stale on-screen balances when a retroactive
grant also touches crystal (or when no grants fire and the conditional
guard would have been the only crystal entry).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds BattlePassSalesPeriodInfoDto, BattlePassProductDto, BattlePassItemListResponse DTOs,
GetItemListAsync on BattlePassService (one product if not premium + CanPurchase, empty if
already premium or off-season), and the /battle_pass/item_list controller action.
2 new integration tests; all 408 pass.
GetOrCreateProgressAsync now persists the new row itself and catches
DbUpdateException on unique-constraint violations — concurrent /info
calls no longer throw 500s. BattlePassService no longer calls
SaveChangesAsync after the get-or-create. FormatWireDate uses a named
JstOffset constant instead of an inline TimeSpan.FromHours(9).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Also fixes BattlePassRepository.GetActiveSeasonAsync to use client-side
DateTimeOffset filtering (SQLite provider cannot translate DateTimeOffset
comparisons in LINQ WHERE/ORDER BY clauses).