diff --git a/SVSim.Database/Services/Inventory/InventoryTransaction.cs b/SVSim.Database/Services/Inventory/InventoryTransaction.cs index f49a1fb..cb82d66 100644 --- a/SVSim.Database/Services/Inventory/InventoryTransaction.cs +++ b/SVSim.Database/Services/Inventory/InventoryTransaction.cs @@ -9,6 +9,8 @@ namespace SVSim.Database.Services.Inventory; internal sealed class InventoryTransaction : IInventoryTransaction { + private const int AcquireHistoryRetention = 300; + private readonly SVSimDbContext _db; private readonly IDbContextTransaction _dbTx; private readonly ILogger _log; @@ -282,6 +284,8 @@ internal sealed class InventoryTransaction : IInventoryTransaction WriteAcquireHistory(); await _db.SaveChangesAsync(ct); + await PruneAcquireHistoryAsync(ct); + await _dbTx.CommitAsync(ct); _committed = true; @@ -290,6 +294,22 @@ internal sealed class InventoryTransaction : IInventoryTransaction return new InventoryCommitResult(rewardList, deltas); } + private async Task PruneAcquireHistoryAsync(CancellationToken ct) + { + var overflowIds = await _db.ViewerAcquireHistory + .Where(h => h.ViewerId == Viewer.Id) + .OrderByDescending(h => h.AcquireTime).ThenByDescending(h => h.Id) + .Skip(AcquireHistoryRetention) + .Select(h => h.Id) + .ToListAsync(ct); + + if (overflowIds.Count == 0) return; + + await _db.ViewerAcquireHistory + .Where(h => overflowIds.Contains(h.Id)) + .ExecuteDeleteAsync(ct); + } + private void WriteAcquireHistory() { var now = DateTime.UtcNow; diff --git a/SVSim.UnitTests/Services/Inventory/InventoryHistoryTests.cs b/SVSim.UnitTests/Services/Inventory/InventoryHistoryTests.cs index cadc249..34a4231 100644 --- a/SVSim.UnitTests/Services/Inventory/InventoryHistoryTests.cs +++ b/SVSim.UnitTests/Services/Inventory/InventoryHistoryTests.cs @@ -241,4 +241,38 @@ public class InventoryHistoryTests .Where(h => h.ViewerId == viewerId).FirstAsync(); Assert.That(row.RewardDetailId, Is.EqualTo(0)); } + + [Test] + public async Task Commit_prunes_history_above_retention_cap_per_viewer() + { + using var factory = new SVSim.UnitTests.Infrastructure.SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(steamId: 76_561_198_000_000_001UL); + long otherViewerId = await factory.SeedViewerAsync(steamId: 76_561_198_000_000_002UL); + using var scope = factory.Services.CreateScope(); + var inv = scope.ServiceProvider.GetRequiredService(); + + // Pre-seed 305 rows for the primary viewer via 305 single-grant commits. + for (int i = 0; i < 305; i++) + { + await using var tx = await inv.BeginAsync(viewerId, configure: c => c.Source = GrantSource.DailyBonus); + await tx.GrantAsync(SVSim.Database.Enums.UserGoodsType.Rupy, 0, 1); + await tx.CommitAsync(); + } + + // Seed 50 rows for an unrelated viewer to verify pruning is per-viewer. + for (int i = 0; i < 50; i++) + { + await using var tx = await inv.BeginAsync(otherViewerId, configure: c => c.Source = GrantSource.DailyBonus); + await tx.GrantAsync(SVSim.Database.Enums.UserGoodsType.Rupy, 0, 1); + await tx.CommitAsync(); + } + + using var verifyScope = factory.Services.CreateScope(); + var ctx = verifyScope.ServiceProvider.GetRequiredService(); + var primaryCount = await ctx.ViewerAcquireHistory.CountAsync(h => h.ViewerId == viewerId); + var otherCount = await ctx.ViewerAcquireHistory.CountAsync(h => h.ViewerId == otherViewerId); + + Assert.That(primaryCount, Is.EqualTo(300), "primary viewer pruned to cap"); + Assert.That(otherCount, Is.EqualTo(50), "other viewer untouched by primary's prune"); + } }