fix(pack): daily half-off single spends crystals and gates re-purchase

The DAILY child gacha (type_detail=3, is_daily_single=true) is the once-per-day
half-off crystal single-pack. Two bugs:

1. /pack/open Daily case was calling TrySpendAsync(Rupee) — decompiled client
   (GachaUI.cs:1046 CheckBuyPackWithCrystal, :1187 SetClystalConfirmDialog)
   confirms it's a crystal purchase. Freeplay mode masked the mismatch in
   testing. Swapped to Crystal + insufficient_crystals error.

2. /pack/info kept emitting is_daily_single=true after the viewer's first
   claim, so the client re-showed the half-off button and a second click
   400'd with daily_free_already_claimed. Added a per-parent-pack gate that
   flips is_daily_single false when LastDailyFreeAt.Date == today. The child
   entry itself stays so the CRYSTAL_MULTI full-price button still activates.

TODO(daily-reset) comments now reference docs/plans/daily-reset-service.md
(in the outer repo) — the current UTC-midnight comparison will migrate to
the JST 02:00 reset boundary via that plan.

Updated the existing daily test to use cost=50 and assert
Crystals: 200 -> 150 + Rupees untouched (previously used cost=0 so the
currency choice was undetectable). Added two /pack/info gate tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-07-04 11:23:51 -04:00
parent 4ce1cd172a
commit 81fe842c15
3 changed files with 118 additions and 9 deletions

View File

@@ -399,8 +399,10 @@ public class PackControllerOpenTests
}
[Test]
public async Task Open_daily_marks_last_daily_free_at_and_rejects_second_attempt()
public async Task Open_daily_debits_crystals_and_rejects_second_attempt()
{
// DAILY is the once-per-day half-off CRYSTAL single-pack (GachaUI.cs:1046 →
// CheckBuyPackWithCrystal). Cost=50 crystals; second same-day attempt 400s.
using var factory = new SVSimTestFactory();
long viewerId = await factory.SeedViewerAsync();
using (var scope = factory.Services.CreateScope())
@@ -412,8 +414,11 @@ public class PackControllerOpenTests
Id = 10001, BasePackId = baseId, PackCategory = PackCategory.None,
CommenceDate = DateTime.UtcNow.AddDays(-1), CompleteDate = DateTime.UtcNow.AddDays(30),
GachaType = 1, GachaDetail = "daily test",
ChildGachas = { new PackChildGachaEntry { GachaId = 200001, TypeDetail = CardPackType.Daily, Cost = 0, CardCount = 1, IsDailySingle = true } },
ChildGachas = { new PackChildGachaEntry { GachaId = 200001, TypeDetail = CardPackType.Daily, Cost = 50, CardCount = 1, IsDailySingle = true } },
});
var v = await db.Viewers.FirstAsync(x => x.Id == viewerId);
v.Currency.Crystals = 200;
v.Currency.Rupees = 200;
await db.SaveChangesAsync();
}
@@ -424,9 +429,17 @@ public class PackControllerOpenTests
var first = await client.PostAsync("/pack/open", JsonBody(json));
Assert.That(first.StatusCode, Is.EqualTo(HttpStatusCode.OK), await first.Content.ReadAsStringAsync());
using (var scope = factory.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
var v = await db.Viewers.FirstAsync(x => x.Id == viewerId);
Assert.That(v.Currency.Crystals, Is.EqualTo(150UL), "200 starting - 50 daily-single crystal cost");
Assert.That(v.Currency.Rupees, Is.EqualTo(200UL), "Rupees must not be touched by a DAILY (crystal) open");
}
var second = await client.PostAsync("/pack/open", JsonBody(json));
Assert.That(second.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest),
"Second daily-free open the same UTC day should be rejected.");
"Second daily-single open the same UTC day should be rejected.");
}
// ---------------- Regression tests for wire-shape quirks ----------------