feat(inventory): scaffold InventoryService namespace types
Empty interfaces + records for IInventoryService, IInventoryTransaction, InventoryCommitResult, InventoryLoadConfig, InventoryCatalogException. Implementation lands in subsequent commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
28
SVSim.Database/Services/Inventory/IInventoryService.cs
Normal file
28
SVSim.Database/Services/Inventory/IInventoryService.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using SVSim.Database.Models;
|
||||||
|
using SVSim.Database.Services;
|
||||||
|
|
||||||
|
namespace SVSim.Database.Services.Inventory;
|
||||||
|
|
||||||
|
public interface IInventoryService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Loads the viewer with the canonical inventory graph (Cards.Card, Sleeves, Emblems,
|
||||||
|
/// LeaderSkins, Degrees, MyPageBackgrounds, Items.Item under AsSplitQuery), opens a DB
|
||||||
|
/// transaction, and returns a builder for queueing operations. Throws
|
||||||
|
/// <see cref="InventoryViewerNotFoundException"/> if the viewer does not exist.
|
||||||
|
/// </summary>
|
||||||
|
Task<IInventoryTransaction> BeginAsync(
|
||||||
|
long viewerId,
|
||||||
|
CancellationToken ct = default,
|
||||||
|
Action<InventoryLoadConfig>? configure = null);
|
||||||
|
|
||||||
|
Task<IReadOnlyList<OwnedCardEntry>> EffectiveOwnedCardsAsync(Viewer viewer, CancellationToken ct = default);
|
||||||
|
Task<EffectiveCosmetics> EffectiveCosmeticsAsync(Viewer viewer, CancellationToken ct = default);
|
||||||
|
long EffectiveBalance(Viewer viewer, SpendCurrency currency);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class InventoryViewerNotFoundException : Exception
|
||||||
|
{
|
||||||
|
public InventoryViewerNotFoundException(long viewerId)
|
||||||
|
: base($"Viewer {viewerId} not found") { }
|
||||||
|
}
|
||||||
30
SVSim.Database/Services/Inventory/IInventoryTransaction.cs
Normal file
30
SVSim.Database/Services/Inventory/IInventoryTransaction.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using SVSim.Database.Enums;
|
||||||
|
using SVSim.Database.Models;
|
||||||
|
using SVSim.Database.Services;
|
||||||
|
|
||||||
|
namespace SVSim.Database.Services.Inventory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Scoped builder returned by <see cref="IInventoryService.BeginAsync"/>. Queue spend +
|
||||||
|
/// grant operations; commit to save and assemble the <see cref="InventoryCommitResult"/>.
|
||||||
|
/// <para>
|
||||||
|
/// Dispose without committing rolls back the underlying DB transaction and detaches any
|
||||||
|
/// in-memory mutations. <b>Always</b> wrap in <c>await using</c>.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public interface IInventoryTransaction : IAsyncDisposable
|
||||||
|
{
|
||||||
|
Viewer Viewer { get; }
|
||||||
|
bool IsFreeplay { get; }
|
||||||
|
|
||||||
|
Task<SpendResult> TrySpendAsync(SpendCurrency currency, long cost, CancellationToken ct = default);
|
||||||
|
Task<SpendResult> TryDebitAsync(UserGoodsType type, long detailId, int num, CancellationToken ct = default);
|
||||||
|
Task<IReadOnlyList<GrantedReward>> GrantAsync(UserGoodsType type, long detailId, int num, CancellationToken ct = default);
|
||||||
|
Task<int> BackfillCardCosmeticsAsync(CancellationToken ct = default);
|
||||||
|
|
||||||
|
long EffectiveBalance(SpendCurrency currency);
|
||||||
|
bool OwnsCard(long cardId);
|
||||||
|
bool OwnsCosmetic(CosmeticType type, int id);
|
||||||
|
|
||||||
|
Task<InventoryCommitResult> CommitAsync(CancellationToken ct = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace SVSim.Database.Services.Inventory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Thrown when an inventory operation references a catalog id that doesn't exist
|
||||||
|
/// (unknown card / item / cosmetic). Programmer error — bubbles to the global error handler.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class InventoryCatalogException : Exception
|
||||||
|
{
|
||||||
|
public InventoryCatalogException(string message) : base(message) { }
|
||||||
|
}
|
||||||
20
SVSim.Database/Services/Inventory/InventoryCommitResult.cs
Normal file
20
SVSim.Database/Services/Inventory/InventoryCommitResult.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using SVSim.Database.Services;
|
||||||
|
|
||||||
|
namespace SVSim.Database.Services.Inventory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Result of <see cref="IInventoryTransaction.CommitAsync"/>.
|
||||||
|
/// <para>
|
||||||
|
/// <see cref="RewardList"/> — wire-shape entries with currency-collision resolved (one entry per
|
||||||
|
/// (type, id); for currencies that were both spent and granted, the last post-state in op order
|
||||||
|
/// wins). Use this for response <c>reward_list</c> fields.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// <see cref="Deltas"/> — verbatim ordered (type, id, num) sequence the caller queued. No
|
||||||
|
/// collapse, no cosmetic-cascade entries. Use this for BP <c>achieved_info</c> and Story
|
||||||
|
/// <c>story_reward_list</c> popups.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public sealed record InventoryCommitResult(
|
||||||
|
IReadOnlyList<GrantedReward> RewardList,
|
||||||
|
IReadOnlyList<GrantedReward> Deltas);
|
||||||
31
SVSim.Database/Services/Inventory/InventoryLoadConfig.cs
Normal file
31
SVSim.Database/Services/Inventory/InventoryLoadConfig.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using System.Linq.Expressions;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Query;
|
||||||
|
using SVSim.Database.Models;
|
||||||
|
|
||||||
|
namespace SVSim.Database.Services.Inventory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Caller-supplied extra <c>.Include</c> chains on top of the canonical viewer-inventory query
|
||||||
|
/// in <see cref="IInventoryService.BeginAsync"/>. Use to bring in extra collections needed by
|
||||||
|
/// the calling controller (e.g. <c>MissionData</c>, <c>BuildDeckPurchases</c>).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class InventoryLoadConfig
|
||||||
|
{
|
||||||
|
internal List<Func<IQueryable<Viewer>, IQueryable<Viewer>>> Includes { get; } = new();
|
||||||
|
|
||||||
|
public InventoryLoadConfig WithInclude<TProperty>(
|
||||||
|
Expression<Func<Viewer, TProperty>> path)
|
||||||
|
{
|
||||||
|
Includes.Add(q => q.Include(path));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InventoryLoadConfig WithInclude<TProperty, TThen>(
|
||||||
|
Expression<Func<Viewer, IEnumerable<TProperty>>> collectionPath,
|
||||||
|
Expression<Func<TProperty, TThen>> thenPath)
|
||||||
|
{
|
||||||
|
Includes.Add(q => q.Include(collectionPath).ThenInclude(thenPath));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user