refactor(pack): route currency spend through CurrencySpendService (freeplay)

This commit is contained in:
gamer147
2026-05-29 14:10:50 -04:00
parent a3a49077b5
commit 163299504a
2 changed files with 38 additions and 16 deletions

View File

@@ -9,6 +9,7 @@ using SVSim.EmulatedEntrypoint.Models.Dtos;
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Pack;
using SVSim.EmulatedEntrypoint.Models.Dtos.Responses.Pack;
using SVSim.Database.Services;
using SVSim.EmulatedEntrypoint.Services;
namespace SVSim.EmulatedEntrypoint.Controllers;
@@ -29,6 +30,8 @@ public class PackController : SVSimController
private readonly SVSimDbContext _db;
private readonly ICardAcquisitionService _acquisition;
private readonly IGachaPointService _gachaPoint;
private readonly ICurrencySpendService _spend;
private readonly IViewerEntitlements _entitlements;
public PackController(
IPackRepository packs,
@@ -37,7 +40,9 @@ public class PackController : SVSimController
IRandom rng,
SVSimDbContext db,
ICardAcquisitionService acquisition,
IGachaPointService gachaPoint)
IGachaPointService gachaPoint,
ICurrencySpendService spend,
IViewerEntitlements entitlements)
{
_packs = packs;
_opener = opener;
@@ -46,6 +51,8 @@ public class PackController : SVSimController
_db = db;
_acquisition = acquisition;
_gachaPoint = gachaPoint;
_spend = spend;
_entitlements = entitlements;
}
[HttpPost("info")]
@@ -292,18 +299,16 @@ public class PackController : SVSimController
{
case 2: // CRYSTAL_MULTI
{
ulong cost = (ulong)child.Cost * (ulong)packNumber;
if (viewer.Currency.Crystals < cost)
return BadRequest(new { error = "insufficient_crystals" });
viewer.Currency.Crystals -= cost;
long cost = (long)child.Cost * packNumber;
var r = await _spend.TrySpendAsync(viewer, SpendCurrency.Crystal, cost);
if (!r.Success) return BadRequest(new { error = "insufficient_crystals" });
break;
}
case 7: // RUPY_MULTI
{
ulong cost = (ulong)child.Cost * (ulong)packNumber;
if (viewer.Currency.Rupees < cost)
return BadRequest(new { error = "insufficient_rupees" });
viewer.Currency.Rupees -= cost;
long cost = (long)child.Cost * packNumber;
var r = await _spend.TrySpendAsync(viewer, SpendCurrency.Rupee, cost);
if (!r.Success) return BadRequest(new { error = "insufficient_rupees" });
break;
}
case 3: // DAILY single — once per UTC day
@@ -315,10 +320,9 @@ public class PackController : SVSimController
if (existing?.LastDailyFreeAt is DateTime last && last.Date == now.Date)
return BadRequest(new { error = "daily_free_already_claimed" });
ulong cost = (ulong)child.Cost * (ulong)packNumber;
if (cost > 0 && viewer.Currency.Rupees < cost)
return BadRequest(new { error = "insufficient_rupees" });
if (cost > 0) viewer.Currency.Rupees -= cost;
long cost = (long)child.Cost * packNumber;
var r = await _spend.TrySpendAsync(viewer, SpendCurrency.Rupee, cost);
if (!r.Success) return BadRequest(new { error = "insufficient_rupees" });
break;
}
}
@@ -359,14 +363,13 @@ public class PackController : SVSimController
// Currency reward entries only apply to purchasable packs; tutorial path omits them.
if (!isTutorialPath)
{
var postViewer = await _db.Viewers.FirstAsync(v => v.Id == viewerId);
if (child.TypeDetail == 2)
{
rewardList.Add(new RewardListEntry { RewardType = 2, RewardId = 0, RewardNum = (int)postViewer.Currency.Crystals });
rewardList.Add(new RewardListEntry { RewardType = 2, RewardId = 0, RewardNum = (int)_entitlements.EffectiveBalance(viewer, SpendCurrency.Crystal) });
}
else if (child.TypeDetail == 7 || child.TypeDetail == 3)
{
rewardList.Add(new RewardListEntry { RewardType = 9, RewardId = 0, RewardNum = (int)postViewer.Currency.Rupees });
rewardList.Add(new RewardListEntry { RewardType = 9, RewardId = 0, RewardNum = (int)_entitlements.EffectiveBalance(viewer, SpendCurrency.Rupee) });
}
}
rewardList.AddRange(grant.RewardList);

View File

@@ -524,6 +524,25 @@ public class PackControllerOpenTests
Assert.That(v.GachaPointBalances.Single().Points, Is.EqualTo(3));
}
[Test]
public async Task Open_freeplay_succeeds_with_zero_balance_and_no_deduction()
{
using var factory = new SVSimTestFactory();
long viewerId = await factory.SeedViewerAsync();
await SeedOpenablePack(factory, viewerId, rupees: 0); // broke, but freeplay
await factory.EnableFreeplayAsync();
using var client = factory.CreateAuthenticatedClient(viewerId);
var json = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","parent_gacha_id":10001,"gacha_id":400002,"gacha_type":1,"pack_number":1,"exclude_card_ids":[]}""";
var response = await client.PostAsync("/pack/open", JsonBody(json));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), await response.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.Rupees, Is.EqualTo(0UL), "freeplay must not deduct real DB balance");
}
[Test]
public async Task TutorialPackOpen_does_not_accrue_gacha_points()
{