Commit Graph

64 Commits

Author SHA1 Message Date
gamer147
39b38e3c80 Battlepass fix 2026-05-28 00:54:46 -04:00
gamer147
7ef5f03eb3 feat(spot-card-exchange): /spot_card_exchange/{top,exchange} + SpotPoints currency
Final shop family. Schema additions:
- ViewerCurrency.SpotPoints (ulong) — new currency column on Viewers.
- SpotCardExchangeEntry — catalog (distinct from the pre-existing
  SpotCardEntry, which is the /load/index rental-cost concept).
- ViewerSpotCardExchange — standalone composite-PK table tracking
  (viewer, card, exchanged_at, is_pre_release_snapshot). Standalone
  avoids cartesian-explode on viewer-graph reads.

RewardGrantService gains a SpotCardPoint=12 currency case mirroring
the RedEther/Crystal pattern. Doc comment refreshed; SpotCard=11 and
SpotCardOnlyLatestCardPack=13 remain unimplemented with explanatory
NotSupportedException — captures show emitters always use Card=5 with
the spot-card-specific id.

Controller:
- /top: emits exactly 9 clan buckets [{"1": [cards]}, ...] matching
  prod's arbitrary single-key shape. exchange_status per-card (0=
  available, 1=already-exchanged, 2=LimitOver after pre-release cap).
  pre_relase_info WIRE TYPO PRESERVED ("relase" not "release").
- /exchange: server-authoritative price (client-supplied
  exchange_point ignored); debits SpotPoints with post-state-total
  reward_list entry; grants card via RewardGrantService.ApplyAsync
  (cosmetic cascade included); persists ViewerSpotCardExchange row.
  Insufficient points / already-exchanged / pre-release-limit all
  return 400 without partial state.

LoadController now populates /load/index spot_point from
viewer.Currency.SpotPoints (was always 0).

PreReleaseLimit hardcoded to 2 matching capture; promote to GameConfig
when captures show variance.

504 tests pass (was 496; +8 spot-card-exchange tests).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 23:23:07 -04:00
gamer147
a5999a3e9c feat(leader-skin): shop catalog + 5 endpoints (/products, /buy, /buy_set, /buy_set_item, /ids)
Schema: LeaderSkinShopSeries -> Products (owned rewards) + owned
SetCompletionRewards on the series; ViewerLeaderSkinSetClaim composite
PK (ViewerId, SeriesId) backs the /buy_set_item idempotent-claim check.

Importer mirrors SleeveShopImporter: idempotent find-or-create, owned
collections rewritten wholesale on rerun. 16 series, 104 products.

Controller (extends existing /set with 5 new endpoints):
- /products: dict-keyed-by-series_id-string wire shape. is_completed
  per-viewer, rewards.status from ViewerLeaderSkinSetClaim (0=no set
  sale, 1=available, 2=claimed) matching client RewardStatus enum.
- /buy: single skin, sales_type 1/2 dispatch, 3=>501.
- /buy_set: whole series at SetPrice; requires set_sales_status != 0;
  grants every product's rewards (RewardGrantService idempotent on
  already-owned cosmetics, so partial-set buys don't double-add).
- /buy_set_item: requires viewer owns every skin in series; idempotent
  on re-claim (returns 200 + empty reward_list, not 400) so client
  retries don't error.
- /ids: flat owned-skin-id list for badge refresh.

496 tests pass (was 486; +10 leader-skin-shop tests).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 22:55:09 -04:00
gamer147
559a170957 feat(item-purchase): /item_purchase/{info,purchase} + catalog
Schema: ItemPurchaseCatalogEntry (single table). Per-viewer quota tracked
via existing ViewerEventCounter keyed by "item_purchase:<id>" with period
JstPeriod.MonthKey when IsMonthlyReset else AllTime.

Controller:
- /info returns catalog + per-period rest (server-computed
  max(0, PurchaseLimit - counter)) + user_card_pack_ticket_list (every
  Items.Type==2 row joined to viewer count, zeros included — client
  unconditionally UpdateItemNum's each entry).
- /purchase: sold_out check before currency check (no counter increment
  on currency failure), inline TryDebit covers RedEther/Crystal/Rupy/Item
  with post-state-total reward_list entry, grant via RewardGrantService.
  Request `rest` accepted but ignored (server counter is canonical).

Importer mirrors PaymentItemImporter shape — idempotent find-or-create,
seed-missing rows preserved. 3 entries from the prod capture.

486 tests pass (was 476; +10 item_purchase tests).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 22:41:02 -04:00
gamer147
f237851e42 feat(sleeve): shop catalog + /sleeve/{info,buy} endpoints
Schema: SleeveShopSeries -> SleeveShopProducts -> Rewards (owned).
Migration AddSleeveShop creates 3 tables with FK cascade.

Importer mirrors BuildDeck pattern: find-or-create per series/product,
rewards replaced wholesale on rerun (owned collection). 10 series,
270 products imported from seeds/sleeve-shop.json.

Controller:
- /sleeve/info returns wire-faithful dict-keyed shape
  ({sleeve_list: {<series_id>: {product_info: {<product_id>: ...}}}}).
  is_purchased_product derived from viewer.Sleeves.Contains(sleeve_id).
- /sleeve/buy: sales_type 0=free / 1=crystal / 2=rupy / 3=ticket(501).
  Validates series_product mismatch, currency, already-purchased.
  Currency debited with post-state-total reward_list entry; cosmetic
  grants dispatched through RewardGrantService.ApplyAsync (covers
  sleeve + emblem bundled grants per product).

476 tests pass (was 466; +10 sleeve tests).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 22:09:45 -04:00
gamer147
6a03ff1bf6 feat(items): catalog import with Type + ThumbnailPath columns
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>
2026-05-27 21:44:24 -04:00
gamer147
529fd13668 signup: close two concurrency holes from final review
(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>
2026-05-27 14:46:19 -04:00
gamer147
68367db214 feat(tool/signup): anonymous viewer creation keyed on UDID
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>
2026-05-27 14:24:55 -04:00
gamer147
f85589d208 repo(viewer): restore dropped rationale comment, add link-idempotency assertion
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>
2026-05-27 14:06:35 -04:00
gamer147
30874c681f repo(viewer): add UDID lookup, anonymous register, Steam link helpers
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>
2026-05-27 14:01:02 -04:00
gamer147
5693ec0302 feat(missions): /load/index materializes viewer mission/achievement state
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>
2026-05-27 10:45:31 -04:00
gamer147
640a77ec6c feat(achievements): /achievement/receive_reward — RewardGrantService + level advance + cap
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 10:41:49 -04:00
gamer147
b65a437102 feat(missions): /mission/info, /mission/retire, /mission/change_receive_setting
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>
2026-05-27 10:35:40 -04:00
gamer147
c9534d8fac feat(missions): ViewerMissionStateService — lazy materialize achievements + assign slots
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>
2026-05-27 10:29:30 -04:00
gamer147
aad604a589 feat(missions): MissionProgressService — counter upsert + achievement claimable on threshold
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>
2026-05-27 10:25:13 -04:00
gamer147
b38be1d953 feat(missions): JstPeriod helper — 02:00 JST anchored day/week/month keys
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 10:21:51 -04:00
gamer147
90cc5a9f5d feat(bp-monthly): BattlePassMonthlyMissionImporter, idempotent by (Y, M, order)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 10:18:03 -04:00
gamer147
6db800f286 feat(achievements): AchievementCatalogImporter, idempotent by (type, level)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 10:17:06 -04:00
gamer147
5df1822217 feat(missions): MissionCatalogImporter, idempotent upsert by id
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 10:14:58 -04:00
gamer147
c303d3040d fix(bp): convert seed JST dates to UTC for Postgres timestamp-with-tz
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>
2026-05-27 00:05:48 -04:00
gamer147
9147ab0ec7 feat(bp): AddPointsAsync plumbing + level-cross auto-grant + weekly cap
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 23:47:30 -04:00
gamer147
2cb8c271a8 feat(bp): /battle_pass/buy — crystal-cost + retroactive premium grants
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 23:36:18 -04:00
gamer147
0ceab721e9 feat(bp): /battle_pass/item_list — derives product per active season
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.
2026-05-26 23:26:46 -04:00
gamer147
8a35f8c40b feat(bp): /battle_pass/info — service + controller + 3 tests
Also fixes BattlePassRepository.GetActiveSeasonAsync to use client-side
DateTimeOffset filtering (SQLite provider cannot translate DateTimeOffset
comparisons in LINQ WHERE/ORDER BY clauses).
2026-05-26 23:14:26 -04:00
gamer147
9bec1df52f feat(load-index): populate battle_pass_level_info from IBattlePassService
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>
2026-05-26 22:56:41 -04:00
gamer147
44da54c418 review(bp): wire new importers into SeedGlobalsAsync + consistent test orphan Id
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 22:36:02 -04:00
gamer147
61a9133855 feat(bp): season + reward importers, idempotent + authoritative-per-season 2026-05-26 22:28:41 -04:00
gamer147
95b8f39ea5 refactor(bp): flatten BattlePassLevelEntry — drop misnamed RewardData jsonb
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 21:57:47 -04:00
gamer147
141f34f817 chore(bootstrap): refresh stale GlobalsImporter references in docs/test names 2026-05-26 16:44:54 -04:00
gamer147
c02991a5c2 refactor(bootstrap): finalize seed migration; remove GlobalsImporter and prod-captures plumbing
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>
2026-05-26 16:06:46 -04:00
gamer147
d14a0be2c8 refactor(bootstrap): finalize load-index migration; GlobalsImporter is now a stub
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>
2026-05-26 15:46:36 -04:00
gamer147
87d0001569 refactor(bootstrap): add 7 load-index importers (excluding card lists)
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.
2026-05-26 15:29:57 -04:00
gamer147
34ed8788a4 refactor(bootstrap): migrate build-deck catalog to seed file 2026-05-26 15:16:36 -04:00
gamer147
a71bf6c62b refactor(bootstrap): migrate /pack/info to seed file 2026-05-26 15:02:49 -04:00
gamer147
83298a2d47 refactor(bootstrap): migrate default decks to seed file
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).
2026-05-26 14:44:21 -04:00
gamer147
a5e4f35c32 refactor(bootstrap): migrate mypage-index globals to seed files 2026-05-26 14:31:25 -04:00
gamer147
0da8ebe1c1 refactor(bootstrap): migrate basic puzzles to seed files
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>
2026-05-26 14:16:32 -04:00
gamer147
c23c56d46c refactor(bootstrap): migrate payment items to seed file
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>
2026-05-26 13:59:50 -04:00
gamer147
0b41474968 fix(practice-opponents): guard duplicate ids + cover edge cases 2026-05-26 13:52:12 -04:00
gamer147
40b0de1d51 refactor(bootstrap): migrate practice opponents to seed file
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.
2026-05-26 13:42:59 -04:00
gamer147
f2a1263198 refactor(bootstrap): add seed loader + extractor scaffolding 2026-05-26 13:24:49 -04:00
gamer147
9090086a47 Class leader fixes 2026-05-26 10:01:37 -04:00
gamer147
b6966ece6e Prebuilt deck purchasing and fixes 2026-05-26 09:16:21 -04:00
gamer147
fa0901b776 More story fixes 2026-05-25 19:07:49 -04:00
gamer147
ce8d80559b Story leader fixes 2026-05-25 17:10:08 -04:00
gamer147
016efeea2c DB Cleanup 2026-05-25 16:45:02 -04:00
gamer147
8e913578ff Consolidation 2026-05-25 16:34:24 -04:00
gamer147
5e7a65fe5a Story 2026-05-25 14:36:12 -04:00
gamer147
558e8288eb Puzzles 2026-05-25 12:03:47 -04:00
gamer147
c14408ba06 Seeding reorg 2026-05-24 21:13:15 -04:00