feat(inventory): add ViewerAcquireHistoryEntry entity + DbSet

Adds the ViewerAcquireHistoryEntry model (8 fields: Id, ViewerId,
RewardType, RewardDetailId, RewardCount, AcquireType, Message,
AcquireTime), registers DbSet<ViewerAcquireHistoryEntry> on
SVSimDbContext, configures model (PK, FK cascade to Viewer, MaxLength
64 on Message, composite index on ViewerId/AcquireTime/Id), and adds a
DbSet round-trip integration test.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-06-09 14:19:00 -04:00
parent 82dc877639
commit 0d036e1bff
3 changed files with 73 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
namespace SVSim.Database.Models;
/// <summary>
/// One row per grant emitted by <c>InventoryTransaction.CommitAsync</c>. Rendered as the
/// <c>histories[]</c> array on <c>POST /item_acquire_history/info</c>. Capped at 300 rows
/// per viewer; oldest pruned on commit.
/// </summary>
public sealed class ViewerAcquireHistoryEntry
{
public long Id { get; set; }
public long ViewerId { get; set; }
/// <summary>UserGoodsType cast to int; matches the wire <c>reward_type</c>.</summary>
public int RewardType { get; set; }
/// <summary>Detail id for the goods; 0 for wallet currencies.</summary>
public long RewardDetailId { get; set; }
/// <summary>Delta granted in this row — NOT a post-state total.</summary>
public int RewardCount { get; set; }
/// <summary>GrantSource cast to int; matches the wire <c>acquire_type</c>.</summary>
public int AcquireType { get; set; }
/// <summary>Pre-localized text the client renders verbatim. Capped at 64 chars.</summary>
public string Message { get; set; } = string.Empty;
/// <summary>Server UTC at commit time. Stamped once per <c>CommitAsync</c>, identical across all rows in that commit.</summary>
public DateTime AcquireTime { get; set; }
}

View File

@@ -103,6 +103,7 @@ public class SVSimDbContext : DbContext
public DbSet<ViewerPresent> ViewerPresents => Set<ViewerPresent>();
public DbSet<TutorialPresentEntry> TutorialPresentEntries => Set<TutorialPresentEntry>();
public DbSet<ViewerAcquireHistoryEntry> ViewerAcquireHistory => Set<ViewerAcquireHistoryEntry>();
public DbSet<ArenaTwoPickReward> ArenaTwoPickRewards { get; set; } = null!;
public DbSet<ViewerArenaTwoPickRun> ViewerArenaTwoPickRuns { get; set; } = null!;
@@ -398,6 +399,19 @@ public class SVSimDbContext : DbContext
b.Property(p => p.PresentId).HasMaxLength(64);
});
modelBuilder.Entity<ViewerAcquireHistoryEntry>(b =>
{
b.HasKey(e => e.Id);
b.Property(e => e.Id).ValueGeneratedOnAdd();
b.Property(e => e.Message).HasMaxLength(64).IsRequired();
b.HasOne<Viewer>()
.WithMany()
.HasForeignKey(e => e.ViewerId)
.OnDelete(DeleteBehavior.Cascade);
b.HasIndex(e => new { e.ViewerId, e.AcquireTime, e.Id })
.HasDatabaseName("IX_ViewerAcquireHistory_ViewerId_AcquireTime_Id");
});
base.OnModelCreating(modelBuilder);
}

View File

@@ -1,3 +1,5 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using SVSim.Database.Services.Inventory;
namespace SVSim.UnitTests.Services.Inventory;
@@ -37,4 +39,31 @@ public class InventoryHistoryTests
var cfg = new InventoryLoadConfig { Source = GrantSource.PackOpen };
Assert.That(cfg.Source, Is.EqualTo(GrantSource.PackOpen));
}
[Test]
public async Task ViewerAcquireHistory_DbSet_round_trips_a_row()
{
using var factory = new SVSim.UnitTests.Infrastructure.SVSimTestFactory();
long viewerId = await factory.SeedViewerAsync();
using var scope = factory.Services.CreateScope();
var ctx = scope.ServiceProvider.GetRequiredService<SVSim.Database.SVSimDbContext>();
ctx.ViewerAcquireHistory.Add(new SVSim.Database.Models.ViewerAcquireHistoryEntry
{
ViewerId = viewerId,
RewardType = (int)SVSim.Database.Enums.UserGoodsType.Rupy,
RewardDetailId = 0,
RewardCount = 50,
AcquireType = (int)GrantSource.DailyBonus,
Message = "Daily Bonus",
AcquireTime = new DateTime(2026, 6, 9, 12, 0, 0, DateTimeKind.Utc),
});
await ctx.SaveChangesAsync();
var roundtrip = await ctx.ViewerAcquireHistory.AsNoTracking()
.Where(h => h.ViewerId == viewerId).ToListAsync();
Assert.That(roundtrip, Has.Count.EqualTo(1));
Assert.That(roundtrip[0].RewardCount, Is.EqualTo(50));
Assert.That(roundtrip[0].AcquireType, Is.EqualTo((int)GrantSource.DailyBonus));
}
}