feat(inventory): BeginAsync loads viewer with canonical graph
Includes Cards/Sleeves/Emblems/LeaderSkins/Degrees/MyPageBackgrounds/Items under AsSplitQuery, plus caller-supplied extras via InventoryLoadConfig. Opens a DB transaction and returns an InventoryTransaction shell. All mutation methods throw NotImplementedException until subsequent tasks land them. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
72
SVSim.Database/Services/Inventory/InventoryService.cs
Normal file
72
SVSim.Database/Services/Inventory/InventoryService.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SVSim.Database.Models;
|
||||
using SVSim.Database.Models.Config;
|
||||
using SVSim.Database.Repositories.Card;
|
||||
using SVSim.Database.Repositories.Collectibles;
|
||||
|
||||
namespace SVSim.Database.Services.Inventory;
|
||||
|
||||
public sealed class InventoryService : IInventoryService
|
||||
{
|
||||
private readonly SVSimDbContext _db;
|
||||
private readonly IGameConfigService _config;
|
||||
private readonly ICardRepository _cards;
|
||||
private readonly ICollectionRepository _collection;
|
||||
private readonly ILogger<InventoryService> _log;
|
||||
|
||||
public InventoryService(
|
||||
SVSimDbContext db,
|
||||
IGameConfigService config,
|
||||
ICardRepository cards,
|
||||
ICollectionRepository collection,
|
||||
ILogger<InventoryService> log)
|
||||
{
|
||||
_db = db;
|
||||
_config = config;
|
||||
_cards = cards;
|
||||
_collection = collection;
|
||||
_log = log;
|
||||
}
|
||||
|
||||
public async Task<IInventoryTransaction> BeginAsync(
|
||||
long viewerId,
|
||||
CancellationToken ct = default,
|
||||
Action<InventoryLoadConfig>? configure = null)
|
||||
{
|
||||
var loadCfg = new InventoryLoadConfig();
|
||||
configure?.Invoke(loadCfg);
|
||||
|
||||
IQueryable<Viewer> query = _db.Viewers
|
||||
.Include(v => v.Cards).ThenInclude(c => c.Card)
|
||||
.Include(v => v.Sleeves)
|
||||
.Include(v => v.Emblems)
|
||||
.Include(v => v.LeaderSkins)
|
||||
.Include(v => v.Degrees)
|
||||
.Include(v => v.MyPageBackgrounds)
|
||||
.Include(v => v.Items).ThenInclude(i => i.Item);
|
||||
|
||||
foreach (var include in loadCfg.Includes)
|
||||
query = include(query);
|
||||
|
||||
var viewer = await query
|
||||
.AsSplitQuery()
|
||||
.FirstOrDefaultAsync(v => v.Id == viewerId, ct)
|
||||
?? throw new InventoryViewerNotFoundException(viewerId);
|
||||
|
||||
var freeplay = _config.Get<FreeplayConfig>();
|
||||
var dbTx = await _db.Database.BeginTransactionAsync(ct);
|
||||
|
||||
return new InventoryTransaction(_db, dbTx, viewer, freeplay, _log);
|
||||
}
|
||||
|
||||
// Stubs for later tasks.
|
||||
public Task<IReadOnlyList<OwnedCardEntry>> EffectiveOwnedCardsAsync(Viewer viewer, CancellationToken ct = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<EffectiveCosmetics> EffectiveCosmeticsAsync(Viewer viewer, CancellationToken ct = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public long EffectiveBalance(Viewer viewer, SpendCurrency currency)
|
||||
=> throw new NotImplementedException();
|
||||
}
|
||||
63
SVSim.Database/Services/Inventory/InventoryTransaction.cs
Normal file
63
SVSim.Database/Services/Inventory/InventoryTransaction.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SVSim.Database.Enums;
|
||||
using SVSim.Database.Models;
|
||||
using SVSim.Database.Models.Config;
|
||||
|
||||
namespace SVSim.Database.Services.Inventory;
|
||||
|
||||
internal sealed class InventoryTransaction : IInventoryTransaction
|
||||
{
|
||||
private readonly SVSimDbContext _db;
|
||||
private readonly IDbContextTransaction _dbTx;
|
||||
private readonly ILogger _log;
|
||||
private readonly FreeplayConfig _freeplay;
|
||||
private bool _committed;
|
||||
|
||||
public Viewer Viewer { get; }
|
||||
public bool IsFreeplay => _freeplay.Enabled;
|
||||
|
||||
public InventoryTransaction(
|
||||
SVSimDbContext db,
|
||||
IDbContextTransaction dbTx,
|
||||
Viewer viewer,
|
||||
FreeplayConfig freeplay,
|
||||
ILogger log)
|
||||
{
|
||||
_db = db;
|
||||
_dbTx = dbTx;
|
||||
Viewer = viewer;
|
||||
_freeplay = freeplay;
|
||||
_log = log;
|
||||
}
|
||||
|
||||
// Implementations land in later tasks. Throw NotImplementedException to keep the build green.
|
||||
public Task<SpendResult> TrySpendAsync(SpendCurrency currency, long cost, CancellationToken ct = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<SpendResult> TryDebitAsync(UserGoodsType type, long detailId, int num, CancellationToken ct = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<IReadOnlyList<GrantedReward>> GrantAsync(UserGoodsType type, long detailId, int num, CancellationToken ct = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<int> BackfillCardCosmeticsAsync(CancellationToken ct = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public long EffectiveBalance(SpendCurrency currency) => throw new NotImplementedException();
|
||||
public bool OwnsCard(long cardId) => throw new NotImplementedException();
|
||||
public bool OwnsCosmetic(CosmeticType type, int id) => throw new NotImplementedException();
|
||||
|
||||
public Task<InventoryCommitResult> CommitAsync(CancellationToken ct = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (!_committed)
|
||||
{
|
||||
await _dbTx.RollbackAsync();
|
||||
_db.ChangeTracker.Clear();
|
||||
}
|
||||
await _dbTx.DisposeAsync();
|
||||
}
|
||||
}
|
||||
@@ -86,6 +86,8 @@ public class Program
|
||||
builder.Services.AddScoped<IGachaPointService, GachaPointService>();
|
||||
builder.Services.AddScoped<ICardAcquisitionService, CardAcquisitionService>();
|
||||
builder.Services.AddScoped<RewardGrantService>();
|
||||
builder.Services.AddScoped<SVSim.Database.Services.Inventory.IInventoryService,
|
||||
SVSim.Database.Services.Inventory.InventoryService>();
|
||||
builder.Services.AddScoped<SVSim.Database.Services.IViewerEntitlements, SVSim.Database.Services.ViewerEntitlements>();
|
||||
builder.Services.AddScoped<SVSim.Database.Services.ICurrencySpendService, SVSim.Database.Services.CurrencySpendService>();
|
||||
builder.Services.AddScoped<SVSim.Database.Repositories.BattlePass.IBattlePassRepository,
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SVSim.Database.Services.Inventory;
|
||||
using SVSim.UnitTests.Infrastructure;
|
||||
|
||||
namespace SVSim.UnitTests.Services.Inventory;
|
||||
|
||||
public class InventoryServiceBeginTests
|
||||
{
|
||||
[Test]
|
||||
public async Task BeginAsync_loads_viewer_with_canonical_graph()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var inv = scope.ServiceProvider.GetRequiredService<IInventoryService>();
|
||||
|
||||
await using var tx = await inv.BeginAsync(viewerId);
|
||||
|
||||
Assert.That(tx.Viewer, Is.Not.Null);
|
||||
Assert.That(tx.Viewer.Id, Is.EqualTo(viewerId));
|
||||
Assert.That(tx.Viewer.Cards, Is.Not.Null, "Cards collection must be loaded");
|
||||
Assert.That(tx.Viewer.Sleeves, Is.Not.Null, "Sleeves collection must be loaded");
|
||||
Assert.That(tx.Viewer.Items, Is.Not.Null, "Items collection must be loaded");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task BeginAsync_throws_when_viewer_missing()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var inv = scope.ServiceProvider.GetRequiredService<IInventoryService>();
|
||||
|
||||
Assert.ThrowsAsync<InventoryViewerNotFoundException>(
|
||||
async () => { await inv.BeginAsync(viewerId: 9999); });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task BeginAsync_applies_extra_includes_via_configure()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var inv = scope.ServiceProvider.GetRequiredService<IInventoryService>();
|
||||
|
||||
await using var tx = await inv.BeginAsync(viewerId, configure:
|
||||
cfg => cfg.WithInclude(v => v.MissionData));
|
||||
|
||||
Assert.That(tx.Viewer.MissionData, Is.Not.Null);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user