Previously RegisterAnonymousViewer auto-completed the tutorial, which
prevented the client from ever entering the tutorial flow. SeedViewerAsync
gains a tutorialState parameter (default 100) so existing tests keep
their pre-completed-tutorial assumption.
Adds the portal pair (shadowverse-portal.com deck-builder endpoints) as
anonymous routes on the app server. The translation middleware learns a new
[NoWireEncryption] attribute that skips both AES calls but keeps the rest of
the msgpack + base64 + envelope pipeline intact, matching prod's portal wire
profile observed in data_dumps/traffic_prod_deckcode.ndjson.
Storage is a 3-minute IMemoryCache — codes are anonymous-global, 4-char
lowercase alphanumeric (matches the shortest prod sample). Foil bit is
stripped on mint to match prod's normalize-on-encode behaviour.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add two spec-prescribed tests that the implementation plan missed:
- Create_proceeds_when_client_possession_snapshot_disagrees_with_server
- Protect_then_load_index_emits_is_protected_one
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Five wire-level integration tests covering: flag set, round-trip unset,
401 without auth, 400 unknown_card, and empty-object response shape.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements ICardInventoryRepository.SetProtected — loads only the
owned-cards collection (no decks/currency), guards against the EF
owned-nav Card.Id==0 default-init quirk, and accepts Count=0 rows
(destruct→re-protect round-trip). Covered by 4 new NUnit tests
(flip, unset, zero-count-row, unknown-card error). Full suite: 533/533.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ItemEntry gains Type (client item_type enum, 1=challenge, 2=card-pack
ticket, 3=premium orb, 4=colosseum, 5=orb piece, 6=skin/event ticket,
7=other) and ThumbnailPath. ItemImporter mirrors PaymentItemImporter
shape: find-or-create per item_id, save once, idempotent. Wired into
Bootstrap.Program and SVSimTestFactory.SeedGlobalsAsync. Unblocks
/item_purchase/info (filters card-pack tickets by Type==2) and any
reward grant of UserGoodsType.Item, which previously threw because
the catalog was empty.
466 tests pass (was 461).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
(1) RegisterAnonymousViewer now catches the unique-violation
race (SQLSTATE 23505 on Postgres / code 19 on SQLite) and
re-reads by UDID, returning the existing row instead of
surfacing 500 to the second concurrent /tool/signup caller.
New repo test exercises the back-to-back register path.
(2) Add unique index on SocialAccountConnection (AccountType,
AccountId). The auth handler's find-or-link path claimed
this index existed as the dedup backstop; the claim was
accurate as design intent but the schema was missing. Now
matched. Comment in handler updated.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
POST /tool/signup upserts a Viewer keyed on the resolved request UDID
(via the existing SID->UDID dict). Stashes the viewer on HttpContext so
the translation middleware emits viewer_id/short_udid/udid in
data_headers. Empty data payload -- all signup outputs flow in
data_headers per spec. Idempotent: repeat signups for the same UDID
return the existing viewer.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Review polish on the prior commit (30874c6):
- BuildDefaultViewer extract dropped the "filter out Id=0 placeholders
and dedupe" comment from the leader-skin grant block — restored.
- LinkSteamToViewer test now calls link twice and asserts count stays
at 1, exercising the alreadyLinked short-circuit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Extracts the default-loadout body into a private BuildDefaultViewer
helper shared by the existing Steam-import path and a new
RegisterAnonymousViewer for /tool/signup. LinkSteamToViewer is the
seam SteamSessionAuthenticationHandler will call on first-Steam-touch
of a UDID-keyed viewer.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
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>
Npgsql rejects DateTimeOffset writes to timestamp-with-tz unless offset
is zero. Caught by manual bootstrap against a real Postgres DB; SQLite
test provider didn't enforce this. Converting to UTC post-parse is
semantically lossless — DateTimeOffset comparisons are instant-based.
Co-Authored-By: Claude Opus 4.7 (1M context) <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.
Also fixes BattlePassRepository.GetActiveSeasonAsync to use client-side
DateTimeOffset filtering (SQLite provider cannot translate DateTimeOffset
comparisons in LINQ WHERE/ORDER BY clauses).
Wire IBattlePassService.GetLevelCurveAsync into LoadController so /load/index
emits the 100-entry battle_pass_level_info dict when levels are seeded.
Also adds BattlePassRepository.ResetLevelCurveCache() to bust the process-level
static cache in tests that seed levels after earlier HTTP calls have primed it
with an empty list, and updates SVSimTestFactory.SeedGlobalsAsync + the stale
Index_surfaces_seeded_globals_after_bootstrap assertion accordingly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Final cleanup of the bootstrap-seed refactor (Task 10 of 10):
- Delete the GlobalsImporter no-op stub and its two remaining call sites
(Program.cs and SVSimTestFactory.cs). All work has moved to per-domain
importers since Task 9.
- Drop the --captures CLI flag and CapturesDir / shippedCaptures plumbing
from Program.cs (BootstrapOptions, ParseArgs, PrintUsage). Bootstrap input
is now cards.json + reference CSVs + per-table seed JSON; no more
prod-captures directory.
- Shrink ImporterBase from 141 to 23 lines: LoadCapture, Serialize,
Upsert<T,TKey>, GetInt/GetString/GetBool/GetLong/GetULong all had zero
callers after the seed migration. Only ParseWireDateTime survives (still
used by PaymentItemImporter and MyPageGlobalsImporter for prod-shaped
timestamp strings).
- Remove the prod-captures Content Include glob from SVSim.Bootstrap.csproj
and both prod-captures globs (production + test-fixture overlay) from
SVSim.UnitTests.csproj. Test fixtures now overlay production seeds at
Data/seeds/ via the Task 7 layout exclusively.
Build clean; 391/391 unit tests passing.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Stage 9C of the bootstrap-seed-refactor:
- Add 6 seed DTOs for the card-id-keyed load-index tables (SpotCard,
ReprintedCard, UnlimitedRestriction, LoadingExclusionCard, MaintenanceCard,
FeatureMaintenance).
- Add CardListsImporter: idempotent upsert of the 6 tables, sharing one
Cards FK set for orphan-warning. FeatureMaintenances clear-and-rewrites
(synthetic ordinal Id; no natural key).
- Add RotationFlagUpdater: reads RotationConfig.RotationCardSetIds from the
GameConfigs section (populated by RotationConfigImporter) and flips
CardSet.IsInRotation to match.
- Add RotationConfig.RotationCardSetIds list property + wire it through
RotationConfigImporter. No migration needed (sections are JSON blobs).
- RotationConfigImporter: use legacy local-kind DateTime parse for schedule
windows so the JSON round-trip stays byte-equivalent to GlobalsImporter.
- Strip GlobalsImporter down to a no-op stub (Task 10 will delete it).
- Wire all 9 new importers into Program.cs and SVSimTestFactory.SeedGlobalsAsync,
in the order RotationConfigImporter -> ... -> CardListsImporter -> RotationFlagUpdater.
- Delete prod-captures/load-index-2026-05-23.json.
- Add CardListsImporterTests covering each sub-table, idempotency,
empty-seed handling, orphan-warning, and the clear-and-rewrite path.
Tests: 391 passing (382 baseline + 9 new).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Stage 9B of the bootstrap-seed-refactor: add per-domain importer classes
that consume the load-index seed split produced in Stage 9A.
New importers (each in its own file under SVSim.Bootstrap/Importers/):
- RotationConfigImporter: writes Rotation/Challenge/MyRotationSchedule
GameConfig sections (atomic UpsertSection<T> pattern, copied private-static
from GlobalsImporter so this importer stands alone post-9C).
- MyRotationImporter: settings + abilities (extractor pre-joins on rotation_id).
- AvatarAbilityImporter: per-leader_skin_id ability rows.
- ArenaSeasonImporter: singleton (Id=1) Take Two arena season.
- BattlePassImporter: per-level reward blobs.
- DailyLoginBonusImporter: per-bonus-id campaign blobs.
- PreReleaseInfoImporter: singleton (Id=1) pre-release window.
Seed DTOs under SVSim.Bootstrap/Models/Seed/ mirror the seed JSON via
[JsonPropertyName] snake_case. Raw-JSON columns (reward_data, format_info,
etc.) use JsonElement on the seed and JsonSerializer.Serialize in the
importer.
Tests: 7 new happy-path tests in LoadIndexImporterTests.cs (idempotency
covered by BattlePass spot-check). Full suite: 382/382 passing (375 + 7).
NOT modifying in this stage: GlobalsImporter.cs (Stage 9C strips the old
methods), Program.cs (Stage 9C wires up all 9 importers), SVSimTestFactory
(Stage 9C). Double-writing on bootstrap is expected and OK during 9B.
Extracts /deck/info's default_deck_list into seeds/default-decks.json
via the new extract-default-decks.ps1 PowerShell script and imports
through DefaultDeckImporter. The importer carries the same orphan-
card-id warning the old GlobalsImporter path emitted; production cards
yield 0 orphans. WarnOrphans stays inside GlobalsImporter for now —
SpotCards/ReprintedCards/UnlimitedRestrictions/LoadingExclusionCards
still use it until Task 9.
Part of the bootstrap seed refactor (Task 6).
Replaces GlobalsImporter's ImportPuzzleGroups/Puzzles/Missions methods (plus the
DeriveTargetPuzzleGroupId regex helper) with a dedicated PuzzleImporter that
reads three flat seed JSONs (puzzle-groups, puzzles, puzzle-missions) produced
by the Python extractor. Groups run before puzzles to satisfy the FK; missions
upsert by sequential id. Wired into Program.cs and SVSimTestFactory after
PaymentItemImporter so existing GlobalsImporterPuzzleTests continue to pass
unchanged via SeedGlobalsAsync. The original prod-capture JSONs are deleted now
that the seeds are authoritative.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Lifts ImportPaymentItems out of GlobalsImporter into a dedicated
PaymentItemImporter driven by Data/seeds/payment-items.json. Wired
into Program.cs and SVSimTestFactory.SeedGlobalsAsync after
PracticeOpponentImporter. Drops the prod-capture file in favor of
the extractor pipeline.
Canonical 4-test suite (basic, idempotent, leave-untouched, skip-zero)
keeps the dict-in-sync upsert pattern Task 2 established.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Move /practice/info handling out of GlobalsImporter into a dedicated
PracticeOpponentImporter that reads a normalized JSON seed file
generated by data_dumps/extract/extract-practice-opponents.ps1.