refactor(pack): route currency spend through CurrencySpendService (freeplay)
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user