using SVSim.Database.Enums; using SVSim.Database.Models; using SVSim.Database.Services; namespace SVSim.UnitTests.Services; public class CurrencySpendServiceTests { private sealed class FakeEntitlements : IViewerEntitlements { public bool IsFreeplay { get; init; } public long FreeplayAmount { get; init; } = 99999; public long EffectiveBalance(Viewer viewer, SpendCurrency currency) { if (IsFreeplay && currency != SpendCurrency.SpotPoint) return FreeplayAmount; return currency switch { SpendCurrency.Crystal => (long)viewer.Currency.Crystals, SpendCurrency.Rupee => (long)viewer.Currency.Rupees, SpendCurrency.RedEther => (long)viewer.Currency.RedEther, SpendCurrency.SpotPoint => (long)viewer.Currency.SpotPoints, _ => 0, }; } public bool OwnsCard(Viewer viewer, long cardId) => IsFreeplay; public bool OwnsCosmetic(Viewer viewer, CosmeticType type, int id) => IsFreeplay; public Task> EffectiveOwnedCardsAsync(Viewer viewer, CancellationToken ct = default) => Task.FromResult>(new List()); public Task EffectiveCosmeticsAsync(Viewer viewer, CancellationToken ct = default) => throw new NotSupportedException(); } private static Viewer NewViewer() => new() { Currency = new ViewerCurrency() }; [Test] public async Task Normal_deducts_and_returns_post_state() { var v = NewViewer(); v.Currency.Crystals = 250; var svc = new CurrencySpendService(new FakeEntitlements { IsFreeplay = false }); var r = await svc.TrySpendAsync(v, SpendCurrency.Crystal, 100); Assert.That(r.Success, Is.True); Assert.That(r.PostStateTotal, Is.EqualTo(150)); Assert.That(v.Currency.Crystals, Is.EqualTo(150UL)); } [Test] public async Task Normal_insufficient_does_not_deduct() { var v = NewViewer(); v.Currency.Rupees = 50; var svc = new CurrencySpendService(new FakeEntitlements { IsFreeplay = false }); var r = await svc.TrySpendAsync(v, SpendCurrency.Rupee, 100); Assert.That(r.Success, Is.False); Assert.That(r.Outcome, Is.EqualTo(SpendOutcome.Insufficient)); Assert.That(v.Currency.Rupees, Is.EqualTo(50UL), "no deduction on insufficient funds"); } [Test] public async Task Freeplay_main_currency_succeeds_without_deducting() { var v = NewViewer(); v.Currency.Crystals = 10; var svc = new CurrencySpendService(new FakeEntitlements { IsFreeplay = true }); var r = await svc.TrySpendAsync(v, SpendCurrency.Crystal, 100000); Assert.That(r.Success, Is.True, "freeplay never blocks on affordability"); Assert.That(r.PostStateTotal, Is.EqualTo(99999), "post-state shows the freeplay balance"); Assert.That(v.Currency.Crystals, Is.EqualTo(10UL), "DB balance untouched in freeplay"); } [Test] public async Task Freeplay_spot_points_still_deduct() { var v = NewViewer(); v.Currency.SpotPoints = 300; var svc = new CurrencySpendService(new FakeEntitlements { IsFreeplay = true }); var r = await svc.TrySpendAsync(v, SpendCurrency.SpotPoint, 100); Assert.That(r.Success, Is.True); Assert.That(r.PostStateTotal, Is.EqualTo(200)); Assert.That(v.Currency.SpotPoints, Is.EqualTo(200UL), "spot points are real even in freeplay"); } }