Commit Graph

105 Commits

Author SHA1 Message Date
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
26bb0ac268 auth: link Steam to UDID-keyed viewer on first authenticated request
After /tool/signup, the client has a viewer_id but no Steam social row.
The first authenticated request (typically /check/game_start) carries
the Steam ticket; if the SteamId lookup misses but the UDID resolves
to a viewer, attach the Steam social now. Subsequent requests hit the
fast SteamId path. Closes the CheckController.GameStart TODO that was
blocking fresh-client boot.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:31:06 -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
7be0dabf87 dto: SignupRequest + empty SignupResponse
Request mirrors LoginPostParams (device telemetry); response is empty
because all signup outputs live in data_headers (viewer_id, short_udid,
udid). MessagePackObject + Key mirrors JsonPropertyName per project
convention.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:16:11 -04:00
gamer147
859980af02 wire: echo UDID in DataHeaders on every response
SignUpTask.Parse validates data_headers.udid against Certification.Udid;
mismatch discards the response. Sourced from the same mappedUdid the
translation middleware uses to decrypt — never controller state. Other
endpoints carry the extra key; SignUpTask is the only reader.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:11:47 -04:00
gamer147
c8ee1e487f ext: HttpContext.GetUdid() over SID-mapping service
Mirrors how the translation middleware resolves the per-request UDID;
needed by ToolController.Signup and the SteamSession find-or-link
branch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:07:42 -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
dffd7a9746 db: add nullable Viewer.Udid with partial unique index
Backstop for /tool/signup idempotency: signup-created viewers carry
the client's UDID (the AES key for that client's wire traffic);
admin-imported viewers stay null. Partial unique index allows the
column to coexist with pre-existing null rows.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 13:54:15 -04:00
gamer147
8e35501954 feat(missions): emit progress events on story/finish and practice/finish
Story emits story_chapter_finish:<main|limited|event>:<story_id>.
Practice emits practice_win:<difficulty>:<enemy_class_id> on win only.

Practice catalog rows use opponent NAMES (e.g. practice_win:elite:arisa)
not numeric class_ids, so captured catalog rows won't match yet. The
infrastructure is in place; bridging numeric→name is a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 10:51:05 -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
574e9ca58b feat(missions): MissionAssembler — single DTO builder reused by all 4 endpoints
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>
2026-05-27 10:31:38 -04:00
gamer147
df65b7a9c8 feat(missions): common DTOs (UserMission, UserAchievement, BPMonthlyMission, MissionInfoData)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 10:30:29 -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
6fbf7cbc94 feat(missions): mission catalog + viewer mission repositories
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 10:21:07 -04:00
gamer147
8fd6bc10c1 chore(bootstrap): register 3 mission/achievement importers
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 10:19:57 -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
f486c15d32 seed(bp-monthly): bp-monthly-missions.json — May 2026 (5 rows)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 10:13:51 -04:00
gamer147
8da91783b1 seed(achievements): achievement-catalog.json — 53 tiers / 52 types
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 10:13:12 -04:00
gamer147
6a66170677 seed(missions): mission-catalog.json — 5 missions
Captured from /mission/info responses across all traffic dumps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 10:12:15 -04:00
gamer147
ebba3c0eef feat(missions): wire 6 entities into DbContext + AddMissionsAndAchievements migration
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 10:11:25 -04:00
gamer147
062adefb99 feat(missions): add 3 viewer entities + Viewer collections
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 10:08:11 -04:00
gamer147
9988ed85df feat(missions): add 3 catalog entities (mission, achievement, BP monthly) 2026-05-27 10:01:27 -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
c7dfd43daa review(bp): doc fixes + sales_period_info non-nullable + drop checked cast + TODO
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 00:00:24 -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
4438e81e37 review(bp): post-state reward_list from RewardGrantService, not deltas
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>
2026-05-26 23:42:06 -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
d877febcb8 review(bp): move SaveChanges into repo with race protection; JST constant
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>
2026-05-26 23:22:48 -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
6ed61ea9f1 feat(bp): /battle_pass/info response DTOs — string-typed wire numerics
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 23:05:51 -04:00
gamer147
7abdfe27cb review(bp): drop fragile cast, thread CT, internalize cache reset
- IndexResponse.BattlePassLevelInfo widened to IReadOnlyDictionary<string,BattlePassLevel>?
  so any IReadOnlyDictionary impl (FrozenDictionary, wrapper, etc.) serializes correctly
  instead of silently null-ing via a failed as-cast
- LoadController.Index now takes CancellationToken ct and threads it to GetLevelCurveAsync
  instead of CancellationToken.None
- BattlePassRepository.ResetLevelCurveCache changed from public to internal; added
  InternalsVisibleTo("SVSim.UnitTests") to SVSim.Database.csproj (was absent)
2026-05-26 23:01: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
9043e20646 feat(bp): IBattlePassService skeleton + level-curve method + DI 2026-05-26 22:49:30 -04:00
gamer147
1420c60486 feat(bp): repositories + identity generation for runtime-inserted tables
Add ValueGeneratedOnAdd to ViewerBattlePassProgress.Id and
ViewerBattlePassClaims.Id so Postgres generates IDENTITY values at
runtime. Regenerate AddBattlePass migration in-place to include the
IdentityByDefaultColumn annotations. Add IBattlePassRepository /
BattlePassRepository (season lookup + level-curve cache) and
IViewerBattlePassRepository / ViewerBattlePassRepository
(get-or-create progress, claim reads/writes).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 22:40:45 -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
d661b6f44c seed(bp): regenerate from extract-battle-pass.py — season 23 + 143 rewards 2026-05-26 22:16:37 -04:00
gamer147
3f784f4294 feat(bp): wire 4 new entities into DbContext + AddBattlePass migration
Adds DbSets and OnModelCreating config for BattlePassSeasonEntry,
BattlePassRewardEntry, ViewerBattlePassProgressEntry, and
ViewerBattlePassClaimEntry; generates migration 20260527021011_AddBattlePass
with DDL-only CreateTable + CreateIndex calls and no InsertData.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 22:12:55 -04:00
gamer147
faa8c0e6dd feat(bp): add ViewerBattlePassProgressEntry + ViewerBattlePassClaimEntry 2026-05-26 22:07:05 -04:00