diff --git a/SVSim.EmulatedEntrypoint/Controllers/PackController.cs b/SVSim.EmulatedEntrypoint/Controllers/PackController.cs index 70680d4..3241981 100644 --- a/SVSim.EmulatedEntrypoint/Controllers/PackController.cs +++ b/SVSim.EmulatedEntrypoint/Controllers/PackController.cs @@ -163,6 +163,37 @@ public class PackController : SVSimController }; } + [HttpPost("exchange_gacha_point")] + public async Task> ExchangeGachaPoint( + ExchangeGachaPointRequest request) + { + if (!TryGetViewerId(out long viewerId)) return Unauthorized(); + + // Load the viewer with the collections the service mutates (balances, received marker, + // cards, cosmetics). AsSplitQuery per project_ef_split_query memory. + var viewer = await _db.Viewers + .Include(v => v.GachaPointBalances) + .Include(v => v.GachaPointReceived) + .Include(v => v.Cards) + .Include(v => v.Sleeves) + .Include(v => v.Emblems) + .Include(v => v.Degrees) + .Include(v => v.LeaderSkins) + .Include(v => v.MyPageBackgrounds) + .AsSplitQuery() + .FirstAsync(v => v.Id == viewerId); + + var outcome = await _gachaPoint.TryExchangeAsync(viewer, request.ParentGachaId, request.CardId); + if (!outcome.Success) return BadRequest(new { error = outcome.Error }); + + await _db.SaveChangesAsync(); + + return new ExchangeGachaPointResponse + { + RewardList = outcome.RewardList.ToList(), + }; + } + [HttpPost("open")] [HttpPost("/tutorial/pack_open")] public async Task> Open(PackOpenRequest request) diff --git a/SVSim.UnitTests/Controllers/PackControllerGachaPointTests.cs b/SVSim.UnitTests/Controllers/PackControllerGachaPointTests.cs index 4828fe3..c8a4dc5 100644 --- a/SVSim.UnitTests/Controllers/PackControllerGachaPointTests.cs +++ b/SVSim.UnitTests/Controllers/PackControllerGachaPointTests.cs @@ -112,4 +112,112 @@ public class PackControllerGachaPointTests Assert.That(text, Does.Contain("\"is_received\":false")); Assert.That(text, Does.Contain("\"is_display_prize\":false")); } + + [Test] + public async Task ExchangeGachaPoint_grants_card_and_returns_post_state_reward_list() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(); + using (var scope = factory.Services.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + db.Classes.Add(new ClassEntry { Id = 0, Name = "Neutral" }); + var set = new ShadowverseCardSetEntry { Id = 10008, IsInRotation = true }; + db.CardSets.Add(set); + set.Cards.Add(new ShadowverseCardEntry + { + Id = 108041010, Name = "leg", Rarity = Rarity.Legendary, + Class = db.Classes.Local.First(), IsFoil = false, + }); + db.CardCosmeticRewards.Add(new CardCosmeticReward + { + CardId = 108041010, Type = CosmeticType.Emblem, CosmeticId = 1080410100, + }); + db.Packs.Add(new PackConfigEntry + { + Id = 10008, BasePackId = 10008, PackCategory = PackCategory.LegendCardPack, + CommenceDate = DateTime.UtcNow.AddDays(-1), CompleteDate = DateTime.UtcNow.AddDays(30), + GachaPointConfig = new PackGachaPointConfig { ExchangeablePoint = 400, IncreaseGachaPoint = 1 }, + }); + var viewer = await db.Viewers + .Include(v => v.GachaPointBalances) + .FirstAsync(v => v.Id == viewerId); + viewer.GachaPointBalances.Add(new ViewerGachaPointBalance { PackId = 10008, Points = 500 }); + await db.SaveChangesAsync(); + } + + using var client = factory.CreateAuthenticatedClient(viewerId); + var body = JsonBody("""{"card_id":108041010,"parent_gacha_id":10008,"odds_gacha_id":10008,"viewer_id":"0","steam_id":0,"steam_session_ticket":""}"""); + var response = await client.PostAsync("/pack/exchange_gacha_point", body); + var text = await response.Content.ReadAsStringAsync(); + + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), text); + + using var doc = JsonDocument.Parse(text); + var rewardList = doc.RootElement.GetProperty("reward_list"); + Assert.That(rewardList.GetArrayLength(), Is.GreaterThan(0)); + // Verify the card grant entry (type=5/Card) is present with the granted card id. + bool foundCard = false; + foreach (var r in rewardList.EnumerateArray()) + { + if (r.GetProperty("reward_type").GetInt32() == 5 && + r.GetProperty("reward_id").GetInt64() == 108041010) + { + foundCard = true; + break; + } + } + Assert.That(foundCard, Is.True, "card grant entry missing from reward_list"); + + // Verify side-effects. + using (var scope = factory.Services.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + var viewer = await db.Viewers + .Include(v => v.GachaPointBalances) + .Include(v => v.GachaPointReceived) + .Include(v => v.Cards).ThenInclude(c => c.Card) + .AsSplitQuery() + .FirstAsync(v => v.Id == viewerId); + Assert.That(viewer.GachaPointBalances.Single().Points, Is.EqualTo(100)); + Assert.That(viewer.GachaPointReceived.Single().CardId, Is.EqualTo(108041010)); + Assert.That(viewer.Cards.Any(c => c.Card.Id == 108041010), Is.True); + } + } + + [Test] + public async Task ExchangeGachaPoint_rejects_when_balance_insufficient() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(); + using (var scope = factory.Services.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + db.Classes.Add(new ClassEntry { Id = 0, Name = "Neutral" }); + var set = new ShadowverseCardSetEntry { Id = 10008, IsInRotation = true }; + db.CardSets.Add(set); + set.Cards.Add(new ShadowverseCardEntry + { + Id = 108041010, Name = "leg", Rarity = Rarity.Legendary, + Class = db.Classes.Local.First(), IsFoil = false, + }); + db.CardCosmeticRewards.Add(new CardCosmeticReward + { + CardId = 108041010, Type = CosmeticType.Emblem, CosmeticId = 1080410100, + }); + db.Packs.Add(new PackConfigEntry + { + Id = 10008, BasePackId = 10008, PackCategory = PackCategory.LegendCardPack, + CommenceDate = DateTime.UtcNow.AddDays(-1), CompleteDate = DateTime.UtcNow.AddDays(30), + GachaPointConfig = new PackGachaPointConfig { ExchangeablePoint = 400, IncreaseGachaPoint = 1 }, + }); + await db.SaveChangesAsync(); + } + + using var client = factory.CreateAuthenticatedClient(viewerId); + var body = JsonBody("""{"card_id":108041010,"parent_gacha_id":10008,"odds_gacha_id":10008,"viewer_id":"0","steam_id":0,"steam_session_ticket":""}"""); + var response = await client.PostAsync("/pack/exchange_gacha_point", body); + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest), + await response.Content.ReadAsStringAsync()); + } }