diff --git a/SVSim.Database/Services/Inventory/InventoryTransaction.cs b/SVSim.Database/Services/Inventory/InventoryTransaction.cs index 4cd9c79..7cc654e 100644 --- a/SVSim.Database/Services/Inventory/InventoryTransaction.cs +++ b/SVSim.Database/Services/Inventory/InventoryTransaction.cs @@ -17,6 +17,12 @@ internal sealed class InventoryTransaction : IInventoryTransaction public Viewer Viewer { get; } public bool IsFreeplay => _freeplay.Enabled; + private readonly List _ops = new(); + + internal abstract record InventoryOp; + internal sealed record SpendOp(SpendCurrency Currency, long Cost, long PostState) : InventoryOp; + internal sealed record GrantOp(UserGoodsType Type, long DetailId, int Num, int PostStateOrCount, bool IsCascade) : InventoryOp; + public InventoryTransaction( SVSimDbContext db, IDbContextTransaction dbTx, @@ -38,8 +44,41 @@ internal sealed class InventoryTransaction : IInventoryTransaction public Task TryDebitAsync(UserGoodsType type, long detailId, int num, CancellationToken ct = default) => throw new NotImplementedException(); - public Task> GrantAsync(UserGoodsType type, long detailId, int num, CancellationToken ct = default) - => throw new NotImplementedException(); + public async Task> GrantAsync(UserGoodsType type, long detailId, int num, CancellationToken ct = default) + { + ThrowIfCommitted(); + + switch (type) + { + case UserGoodsType.Rupy: + Viewer.Currency.Rupees += (ulong)num; + var rupy = checked((int)Viewer.Currency.Rupees); + _ops.Add(new GrantOp(type, detailId, num, rupy, false)); + return Single(type, detailId, rupy); + + case UserGoodsType.Crystal: + Viewer.Currency.Crystals += (ulong)num; + var crystal = checked((int)Viewer.Currency.Crystals); + _ops.Add(new GrantOp(type, detailId, num, crystal, false)); + return Single(type, detailId, crystal); + + case UserGoodsType.RedEther: + Viewer.Currency.RedEther += (ulong)num; + var red = checked((int)Viewer.Currency.RedEther); + _ops.Add(new GrantOp(type, detailId, num, red, false)); + return Single(type, detailId, red); + + case UserGoodsType.SpotCardPoint: + Viewer.Currency.SpotPoints += (ulong)num; + var spot = checked((int)Viewer.Currency.SpotPoints); + _ops.Add(new GrantOp(type, detailId, num, spot, false)); + return Single(type, detailId, spot); + + default: + throw new NotImplementedException( + $"UserGoodsType {type} grant lands in a subsequent task"); + } + } public Task BackfillCardCosmeticsAsync(CancellationToken ct = default) => throw new NotImplementedException(); @@ -51,6 +90,15 @@ internal sealed class InventoryTransaction : IInventoryTransaction public Task CommitAsync(CancellationToken ct = default) => throw new NotImplementedException(); + private static IReadOnlyList Single(UserGoodsType type, long id, int num) + => new[] { new GrantedReward((int)type, id, num) }; + + private void ThrowIfCommitted() + { + if (_committed) + throw new InvalidOperationException("Inventory transaction already committed"); + } + public async ValueTask DisposeAsync() { if (!_committed) diff --git a/SVSim.UnitTests/Services/Inventory/InventoryGrantCurrencyTests.cs b/SVSim.UnitTests/Services/Inventory/InventoryGrantCurrencyTests.cs new file mode 100644 index 0000000..1c1db7d --- /dev/null +++ b/SVSim.UnitTests/Services/Inventory/InventoryGrantCurrencyTests.cs @@ -0,0 +1,41 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using SVSim.Database; +using SVSim.Database.Enums; +using SVSim.Database.Services.Inventory; +using SVSim.UnitTests.Infrastructure; + +namespace SVSim.UnitTests.Services.Inventory; + +public class InventoryGrantCurrencyTests +{ + [TestCase(UserGoodsType.Rupy)] + [TestCase(UserGoodsType.Crystal)] + [TestCase(UserGoodsType.RedEther)] + [TestCase(UserGoodsType.SpotCardPoint)] + public async Task Grant_currency_adds_and_emits_post_state(UserGoodsType type) + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(); + using var scope = factory.Services.CreateScope(); + var ctx = scope.ServiceProvider.GetRequiredService(); + var v = await ctx.Viewers.FirstAsync(x => x.Id == viewerId); + switch (type) + { + case UserGoodsType.Rupy: v.Currency.Rupees = 100; break; + case UserGoodsType.Crystal: v.Currency.Crystals = 100; break; + case UserGoodsType.RedEther: v.Currency.RedEther = 100; break; + case UserGoodsType.SpotCardPoint: v.Currency.SpotPoints = 100; break; + } + await ctx.SaveChangesAsync(); + + var inv = scope.ServiceProvider.GetRequiredService(); + await using var tx = await inv.BeginAsync(viewerId); + + var granted = await tx.GrantAsync(type, detailId: 0, num: 50); + + Assert.That(granted, Has.Count.EqualTo(1)); + Assert.That(granted[0].RewardType, Is.EqualTo((int)type)); + Assert.That(granted[0].RewardNum, Is.EqualTo(150)); + } +}