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

View File

@@ -524,6 +524,25 @@ public class PackControllerOpenTests
Assert.That(v.GachaPointBalances.Single().Points, Is.EqualTo(3)); 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] [Test]
public async Task TutorialPackOpen_does_not_accrue_gacha_points() public async Task TutorialPackOpen_does_not_accrue_gacha_points()
{ {