fix(gacha-points): look up by odds_gacha_id, not parent_gacha_id
The two wire fields differ for seasonal packs (verified against
traffic_prod_all_gacha_exchange.ndjson — every captured request pairs
odds_gacha_id=16xxx with parent_gacha_id=10xxx). The OLD DTO docstring
assumed they were always equal; today's controller used
ParentGachaId, which lands on the base/family pack id (often a
synthesized disabled stub with no GachaPointConfig) and returns [].
Fix:
- GetGachaPointRewards and ExchangeGachaPoint now consume OddsGachaId.
- Update both DTO docstrings to document the seasonal-pack pattern.
- Regression test seeds (16015 enabled w/ GachaPointConfig, 10015
disabled stub w/o config) and asserts the response uses 16015's
catalog.
Symptom: opening pack 16015 (parent_gacha_id=16015 in /pack/open)
accrued gacha points correctly, but /pack/get_gacha_point_rewards with
{odds_gacha_id:16015, parent_gacha_id:10015} returned an empty list.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -189,7 +189,11 @@ public class PackController : SVSimController
|
||||
{
|
||||
if (!TryGetViewerId(out long viewerId)) return Unauthorized();
|
||||
|
||||
var rewards = await _gachaPoint.GetRewardsAsync(request.ParentGachaId, viewerId);
|
||||
// odds_gacha_id is the active seasonal pack id (the one with GachaPointConfig +
|
||||
// balance). parent_gacha_id is the base_pack_id of the family — not the lookup key.
|
||||
// See GetGachaPointRewardsRequest docstring; verified against
|
||||
// traffic_prod_all_gacha_exchange.ndjson.
|
||||
var rewards = await _gachaPoint.GetRewardsAsync(request.OddsGachaId, viewerId);
|
||||
|
||||
return new GetGachaPointRewardsResponse
|
||||
{
|
||||
@@ -217,7 +221,9 @@ public class PackController : SVSimController
|
||||
.AsSplitQuery()
|
||||
.FirstAsync(v => v.Id == viewerId);
|
||||
|
||||
var outcome = await _gachaPoint.TryExchangeAsync(viewer, request.ParentGachaId, request.CardId);
|
||||
// Use odds_gacha_id (the seasonal pack id) — that's where the balance / received marker
|
||||
// live. Mirrors the GetGachaPointRewards fix.
|
||||
var outcome = await _gachaPoint.TryExchangeAsync(viewer, request.OddsGachaId, request.CardId);
|
||||
if (!outcome.Success) return BadRequest(new { error = outcome.Error });
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
@@ -4,6 +4,11 @@ using SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
|
||||
|
||||
namespace SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Pack;
|
||||
|
||||
/// <summary>
|
||||
/// Inbound /pack/exchange_gacha_point body. See
|
||||
/// <see cref="GetGachaPointRewardsRequest"/> for the odds_gacha_id vs parent_gacha_id split —
|
||||
/// same pattern here: the server consumes <c>odds_gacha_id</c> for the lookup.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public class ExchangeGachaPointRequest : BaseRequest
|
||||
{
|
||||
|
||||
@@ -5,8 +5,14 @@ using SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
|
||||
namespace SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Pack;
|
||||
|
||||
/// <summary>
|
||||
/// Inbound /pack/get_gacha_point_rewards body. Capture shows odds_gacha_id and parent_gacha_id
|
||||
/// are always the same value (the pack id); we only consume parent_gacha_id for the lookup.
|
||||
/// Inbound /pack/get_gacha_point_rewards body.
|
||||
///
|
||||
/// The two ids DIFFER for seasonal packs (e.g. UCL):
|
||||
/// odds_gacha_id = the seasonal "current" pack id (matches /pack/info parent_gacha_id),
|
||||
/// and is where the GachaPointConfig + gacha-point balance live.
|
||||
/// parent_gacha_id = the base/family pack id (matches /pack/info base_pack_id).
|
||||
/// Verified against traffic_prod_all_gacha_exchange.ndjson — every captured request shows
|
||||
/// the pair as (16xxx, 10xxx). Server consumes <c>odds_gacha_id</c> for the lookup.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public class GetGachaPointRewardsRequest : BaseRequest
|
||||
|
||||
@@ -68,6 +68,67 @@ public class PackControllerGachaPointTests
|
||||
Assert.That(rewardList[0].GetProperty("reward_number").GetInt32(), Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetGachaPointRewards_uses_odds_gacha_id_when_it_differs_from_parent_gacha_id()
|
||||
{
|
||||
// Regression for the seasonal-pack case captured in traffic_prod_all_gacha_exchange.ndjson:
|
||||
// the client sends {odds_gacha_id: 16xxx, parent_gacha_id: 10xxx} where odds_gacha_id is
|
||||
// the active seasonal pack (carries GachaPointConfig + balance) and parent_gacha_id is
|
||||
// the base/family pack id (often a disabled stub in our DB). Looking up by parent_gacha_id
|
||||
// would land on the stub and return [].
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
using (var scope = factory.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
db.Classes.Add(new ClassEntry { Id = 0, Name = "Neutral" });
|
||||
var set = new ShadowverseCardSetEntry { Id = 16015, IsInRotation = true };
|
||||
db.CardSets.Add(set);
|
||||
set.Cards.Add(new ShadowverseCardEntry
|
||||
{
|
||||
Id = 115041010, Name = "ucl-leg", Rarity = Rarity.Legendary,
|
||||
Class = db.Classes.Local.First(), IsFoil = false,
|
||||
});
|
||||
db.CardCosmeticRewards.Add(new CardCosmeticReward
|
||||
{
|
||||
CardId = 115041010, Type = CosmeticType.Emblem, CosmeticId = 1150410100,
|
||||
});
|
||||
// Active seasonal pack — has GachaPointConfig.
|
||||
db.Packs.Add(new PackConfigEntry
|
||||
{
|
||||
Id = 16015, BasePackId = 10015, PackCategory = PackCategory.None,
|
||||
CommenceDate = DateTime.UtcNow.AddDays(-1), CompleteDate = DateTime.UtcNow.AddDays(30),
|
||||
GachaType = 1, GachaDetail = "UCL season",
|
||||
GachaPointConfig = new PackGachaPointConfig { ExchangeablePoint = 400, IncreaseGachaPoint = 1 },
|
||||
});
|
||||
// Disabled base/family stub — no GachaPointConfig (matches the synthesized-stub state).
|
||||
db.Packs.Add(new PackConfigEntry
|
||||
{
|
||||
Id = 10015, BasePackId = 10015, PackCategory = PackCategory.None,
|
||||
CommenceDate = DateTime.UtcNow.AddDays(-1), CompleteDate = DateTime.UtcNow.AddDays(30),
|
||||
GachaType = 1, GachaDetail = "UCL family stub", IsEnabled = false,
|
||||
GachaPointConfig = null,
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
await factory.SeedPackDrawTableFromSetAsync(16015, 16015);
|
||||
|
||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||
// Mirrors the prod capture shape — odds_gacha_id is the active pack, parent_gacha_id
|
||||
// is the base/family id (here, a disabled stub).
|
||||
var body = JsonBody("""{"odds_gacha_id":16015,"parent_gacha_id":10015,"viewer_id":"0","steam_id":0,"steam_session_ticket":""}""");
|
||||
var response = await client.PostAsync("/pack/get_gacha_point_rewards", body);
|
||||
|
||||
var text = await response.Content.ReadAsStringAsync();
|
||||
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), text);
|
||||
|
||||
using var doc = JsonDocument.Parse(text);
|
||||
var rewards = doc.RootElement.GetProperty("gacha_point_rewards");
|
||||
Assert.That(rewards.GetArrayLength(), Is.EqualTo(1),
|
||||
"lookup must resolve via odds_gacha_id (16015), not parent_gacha_id (10015)");
|
||||
Assert.That(rewards[0].GetProperty("card_id").GetInt64(), Is.EqualTo(115041010));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetGachaPointRewards_wire_keys_match_prod_capture()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user