diff --git a/SVSim.Database/Services/Inventory/InventoryTransaction.cs b/SVSim.Database/Services/Inventory/InventoryTransaction.cs index 391d8d2..a6337db 100644 --- a/SVSim.Database/Services/Inventory/InventoryTransaction.cs +++ b/SVSim.Database/Services/Inventory/InventoryTransaction.cs @@ -82,7 +82,34 @@ internal sealed class InventoryTransaction : IInventoryTransaction } public Task TryDebitAsync(UserGoodsType type, long detailId, int num, CancellationToken ct = default) - => throw new NotImplementedException(); + { + ThrowIfCommitted(); + return type switch + { + UserGoodsType.Crystal => TrySpendAsync(SpendCurrency.Crystal, num, ct), + UserGoodsType.Rupy => TrySpendAsync(SpendCurrency.Rupee, num, ct), + UserGoodsType.RedEther => TrySpendAsync(SpendCurrency.RedEther, num, ct), + UserGoodsType.SpotCardPoint => TrySpendAsync(SpendCurrency.SpotPoint, num, ct), + UserGoodsType.Item => Task.FromResult(DebitItem(detailId, num)), + _ => throw new NotSupportedException($"Debit not supported for {type}"), + }; + } + + private SpendResult DebitItem(long detailId, int num) + { + var owned = Viewer.Items.FirstOrDefault(i => i.Item.Id == (int)detailId); + if (owned is null) + throw new InventoryCatalogException($"Item {detailId} not owned by viewer"); + if (owned.Count < num) + return new SpendResult(SpendOutcome.Insufficient, owned.Count); + owned.Count -= num; + // Item debit logged as a synthetic SpendOp so CommitAsync can track it. + // Sentinel currency (int)-1 is filtered out by CommitAsync's currency-collision loop. + _ops.Add(new SpendOp((SpendCurrency)(-1) /* sentinel */, num, owned.Count)); + // IsCascade: true so this GrantOp is excluded from BuildDeltas output. + _ops.Add(new GrantOp(UserGoodsType.Item, detailId, 0, owned.Count, IsCascade: true)); + return new SpendResult(SpendOutcome.Success, owned.Count); + } public async Task> GrantAsync(UserGoodsType type, long detailId, int num, CancellationToken ct = default) { diff --git a/SVSim.UnitTests/Services/Inventory/InventoryDebitTests.cs b/SVSim.UnitTests/Services/Inventory/InventoryDebitTests.cs new file mode 100644 index 0000000..5412075 --- /dev/null +++ b/SVSim.UnitTests/Services/Inventory/InventoryDebitTests.cs @@ -0,0 +1,76 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using SVSim.Database; +using SVSim.Database.Enums; +using SVSim.Database.Models; +using SVSim.Database.Services; +using SVSim.Database.Services.Inventory; +using SVSim.UnitTests.Infrastructure; + +namespace SVSim.UnitTests.Services.Inventory; + +public class InventoryDebitTests +{ + [Test] + public async Task Debit_Crystal_delegates_to_TrySpend() + { + 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); + v.Currency.Crystals = 500; + await ctx.SaveChangesAsync(); + + var inv = scope.ServiceProvider.GetRequiredService(); + await using var tx = await inv.BeginAsync(viewerId); + var r = await tx.TryDebitAsync(UserGoodsType.Crystal, 0, 200); + + Assert.That(r.Success, Is.True); + Assert.That(r.PostStateTotal, Is.EqualTo(300)); + } + + [Test] + public async Task Debit_Item_decrements_count_and_returns_post_state() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(); + using var scope = factory.Services.CreateScope(); + var ctx = scope.ServiceProvider.GetRequiredService(); + const int itemId = 32000; + var item = new ItemEntry { Id = itemId }; + ctx.Items.Add(item); + var v = await ctx.Viewers.Include(x => x.Items).ThenInclude(i => i.Item).FirstAsync(x => x.Id == viewerId); + v.Items.Add(new OwnedItemEntry { Item = item, Count = 10, Viewer = v }); + await ctx.SaveChangesAsync(); + + var inv = scope.ServiceProvider.GetRequiredService(); + await using var tx = await inv.BeginAsync(viewerId); + var r = await tx.TryDebitAsync(UserGoodsType.Item, itemId, 3); + + Assert.That(r.Success, Is.True); + Assert.That(r.PostStateTotal, Is.EqualTo(7)); + } + + [Test] + public async Task Debit_Item_insufficient_returns_current_count_and_does_not_decrement() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(); + using var scope = factory.Services.CreateScope(); + var ctx = scope.ServiceProvider.GetRequiredService(); + const int itemId = 32001; + var item = new ItemEntry { Id = itemId }; + ctx.Items.Add(item); + var v = await ctx.Viewers.Include(x => x.Items).ThenInclude(i => i.Item).FirstAsync(x => x.Id == viewerId); + v.Items.Add(new OwnedItemEntry { Item = item, Count = 2, Viewer = v }); + await ctx.SaveChangesAsync(); + + var inv = scope.ServiceProvider.GetRequiredService(); + await using var tx = await inv.BeginAsync(viewerId); + var r = await tx.TryDebitAsync(UserGoodsType.Item, itemId, 5); + + Assert.That(r.Outcome, Is.EqualTo(SpendOutcome.Insufficient)); + Assert.That(r.PostStateTotal, Is.EqualTo(2)); + } +}