feat(inventory): write acquire history rows on commit
CommitAsync now calls WriteAcquireHistory() between the two SaveChanges calls: iterates _ops, skips SpendOps, writes one ViewerAcquireHistoryEntry per GrantOp. Cascade rows get GrantSource.CardCosmeticCascade; wallet currencies zero RewardDetailId; all rows in a single commit share one DateTime.UtcNow timestamp. Closes _source plumbing from Task 5. 5 new tests added (46 total inventory, 0 failures). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -66,4 +66,154 @@ public class InventoryHistoryTests
|
||||
Assert.That(roundtrip[0].RewardCount, Is.EqualTo(50));
|
||||
Assert.That(roundtrip[0].AcquireType, Is.EqualTo((int)GrantSource.DailyBonus));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Commit_writes_one_history_row_per_grant_tagged_with_source()
|
||||
{
|
||||
using var factory = new SVSim.UnitTests.Infrastructure.SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var inv = scope.ServiceProvider.GetRequiredService<SVSim.Database.Services.Inventory.IInventoryService>();
|
||||
|
||||
DateTime before = DateTime.UtcNow;
|
||||
await using (var tx = await inv.BeginAsync(viewerId, configure: c => c.Source = GrantSource.DailyBonus))
|
||||
{
|
||||
await tx.GrantAsync(SVSim.Database.Enums.UserGoodsType.Rupy, 0, 20);
|
||||
await tx.CommitAsync();
|
||||
}
|
||||
DateTime after = DateTime.UtcNow;
|
||||
|
||||
using var verifyScope = factory.Services.CreateScope();
|
||||
var ctx = verifyScope.ServiceProvider.GetRequiredService<SVSim.Database.SVSimDbContext>();
|
||||
var rows = await ctx.ViewerAcquireHistory.AsNoTracking()
|
||||
.Where(h => h.ViewerId == viewerId).ToListAsync();
|
||||
|
||||
Assert.That(rows, Has.Count.EqualTo(1));
|
||||
Assert.That(rows[0].RewardType, Is.EqualTo((int)SVSim.Database.Enums.UserGoodsType.Rupy));
|
||||
Assert.That(rows[0].RewardDetailId, Is.EqualTo(0));
|
||||
Assert.That(rows[0].RewardCount, Is.EqualTo(20));
|
||||
Assert.That(rows[0].AcquireType, Is.EqualTo((int)GrantSource.DailyBonus));
|
||||
Assert.That(rows[0].Message, Is.EqualTo("Daily Bonus"));
|
||||
Assert.That(rows[0].AcquireTime, Is.InRange(before, after));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Commit_writes_multiple_rows_in_order_with_shared_timestamp()
|
||||
{
|
||||
using var factory = new SVSim.UnitTests.Infrastructure.SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var inv = scope.ServiceProvider.GetRequiredService<SVSim.Database.Services.Inventory.IInventoryService>();
|
||||
|
||||
await using (var tx = await inv.BeginAsync(viewerId, configure: c => c.Source = GrantSource.PackOpen))
|
||||
{
|
||||
await tx.GrantAsync(SVSim.Database.Enums.UserGoodsType.Rupy, 0, 1);
|
||||
await tx.GrantAsync(SVSim.Database.Enums.UserGoodsType.Crystal, 0, 2);
|
||||
await tx.GrantAsync(SVSim.Database.Enums.UserGoodsType.RedEther, 0, 3);
|
||||
await tx.CommitAsync();
|
||||
}
|
||||
|
||||
using var verifyScope = factory.Services.CreateScope();
|
||||
var ctx = verifyScope.ServiceProvider.GetRequiredService<SVSim.Database.SVSimDbContext>();
|
||||
var rows = await ctx.ViewerAcquireHistory.AsNoTracking()
|
||||
.Where(h => h.ViewerId == viewerId)
|
||||
.OrderBy(h => h.Id)
|
||||
.ToListAsync();
|
||||
|
||||
Assert.That(rows, Has.Count.EqualTo(3));
|
||||
Assert.That(rows.Select(r => r.RewardCount), Is.EqualTo(new[] { 1, 2, 3 }));
|
||||
Assert.That(rows.Select(r => r.AcquireTime).Distinct().Count(), Is.EqualTo(1),
|
||||
"all rows in one commit share AcquireTime");
|
||||
Assert.That(rows.All(r => r.AcquireType == (int)GrantSource.PackOpen), Is.True);
|
||||
Assert.That(rows.All(r => r.Message == "From buying card packs"), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Commit_writes_no_history_rows_for_spend_only_transactions()
|
||||
{
|
||||
using var factory = new SVSim.UnitTests.Infrastructure.SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
using var seedScope = factory.Services.CreateScope();
|
||||
var seedCtx = seedScope.ServiceProvider.GetRequiredService<SVSim.Database.SVSimDbContext>();
|
||||
var v = await seedCtx.Viewers.FirstAsync(x => x.Id == viewerId);
|
||||
v.Currency.Crystals = 1000;
|
||||
await seedCtx.SaveChangesAsync();
|
||||
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var inv = scope.ServiceProvider.GetRequiredService<SVSim.Database.Services.Inventory.IInventoryService>();
|
||||
await using (var tx = await inv.BeginAsync(viewerId, configure: c => c.Source = GrantSource.ItemPurchase))
|
||||
{
|
||||
await tx.TrySpendAsync(SVSim.Database.Services.SpendCurrency.Crystal, 500);
|
||||
await tx.CommitAsync();
|
||||
}
|
||||
|
||||
using var verifyScope = factory.Services.CreateScope();
|
||||
var ctx = verifyScope.ServiceProvider.GetRequiredService<SVSim.Database.SVSimDbContext>();
|
||||
var rows = await ctx.ViewerAcquireHistory.AsNoTracking()
|
||||
.Where(h => h.ViewerId == viewerId).ToListAsync();
|
||||
Assert.That(rows, Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Commit_writes_cascade_cosmetic_with_distinct_source_and_message()
|
||||
{
|
||||
using var factory = new SVSim.UnitTests.Infrastructure.SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
long cardId = await factory.SeedCardAsync();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var ctx = scope.ServiceProvider.GetRequiredService<SVSim.Database.SVSimDbContext>();
|
||||
const int sleeveId = 2_000_030_001;
|
||||
ctx.Sleeves.Add(new SVSim.Database.Models.SleeveEntry { Id = sleeveId });
|
||||
ctx.CardCosmeticRewards.Add(new SVSim.Database.Models.CardCosmeticReward
|
||||
{
|
||||
CardId = (int)cardId,
|
||||
CosmeticId = sleeveId,
|
||||
Type = SVSim.Database.Enums.CosmeticType.Sleeve,
|
||||
});
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
var inv = scope.ServiceProvider.GetRequiredService<SVSim.Database.Services.Inventory.IInventoryService>();
|
||||
await using (var tx = await inv.BeginAsync(viewerId, configure: c => c.Source = GrantSource.PackOpen))
|
||||
{
|
||||
await tx.GrantAsync(SVSim.Database.Enums.UserGoodsType.Card, cardId, 1);
|
||||
await tx.CommitAsync();
|
||||
}
|
||||
|
||||
using var verifyScope = factory.Services.CreateScope();
|
||||
var ctx2 = verifyScope.ServiceProvider.GetRequiredService<SVSim.Database.SVSimDbContext>();
|
||||
var rows = await ctx2.ViewerAcquireHistory.AsNoTracking()
|
||||
.Where(h => h.ViewerId == viewerId)
|
||||
.OrderBy(h => h.Id)
|
||||
.ToListAsync();
|
||||
|
||||
Assert.That(rows, Has.Count.EqualTo(2), "card grant + cascade sleeve grant");
|
||||
Assert.That(rows[0].RewardType, Is.EqualTo((int)SVSim.Database.Enums.UserGoodsType.Card));
|
||||
Assert.That(rows[0].AcquireType, Is.EqualTo((int)GrantSource.PackOpen));
|
||||
Assert.That(rows[0].Message, Is.EqualTo("From buying card packs"));
|
||||
Assert.That(rows[1].RewardType, Is.EqualTo((int)SVSim.Database.Enums.UserGoodsType.Sleeve));
|
||||
Assert.That(rows[1].AcquireType, Is.EqualTo((int)GrantSource.CardCosmeticCascade));
|
||||
Assert.That(rows[1].Message, Is.EqualTo("Card cosmetic"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Commit_zero_pads_detail_id_for_wallet_currencies()
|
||||
{
|
||||
using var factory = new SVSim.UnitTests.Infrastructure.SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var inv = scope.ServiceProvider.GetRequiredService<SVSim.Database.Services.Inventory.IInventoryService>();
|
||||
|
||||
await using (var tx = await inv.BeginAsync(viewerId, configure: c => c.Source = GrantSource.DailyBonus))
|
||||
{
|
||||
// detailId=99 is meaningful for some types but ignored for wallets — should still write 0.
|
||||
await tx.GrantAsync(SVSim.Database.Enums.UserGoodsType.Crystal, 99, 5);
|
||||
await tx.CommitAsync();
|
||||
}
|
||||
|
||||
using var verifyScope = factory.Services.CreateScope();
|
||||
var ctx = verifyScope.ServiceProvider.GetRequiredService<SVSim.Database.SVSimDbContext>();
|
||||
var row = await ctx.ViewerAcquireHistory.AsNoTracking()
|
||||
.Where(h => h.ViewerId == viewerId).FirstAsync();
|
||||
Assert.That(row.RewardDetailId, Is.EqualTo(0));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user