diff --git a/SVSim.Database/Services/Inventory/IInventoryService.cs b/SVSim.Database/Services/Inventory/IInventoryService.cs new file mode 100644 index 0000000..e1d6ce3 --- /dev/null +++ b/SVSim.Database/Services/Inventory/IInventoryService.cs @@ -0,0 +1,28 @@ +using SVSim.Database.Models; +using SVSim.Database.Services; + +namespace SVSim.Database.Services.Inventory; + +public interface IInventoryService +{ + /// + /// 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 + /// if the viewer does not exist. + /// + Task BeginAsync( + long viewerId, + CancellationToken ct = default, + Action? configure = null); + + Task> EffectiveOwnedCardsAsync(Viewer viewer, CancellationToken ct = default); + Task 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") { } +} diff --git a/SVSim.Database/Services/Inventory/IInventoryTransaction.cs b/SVSim.Database/Services/Inventory/IInventoryTransaction.cs new file mode 100644 index 0000000..1ba2059 --- /dev/null +++ b/SVSim.Database/Services/Inventory/IInventoryTransaction.cs @@ -0,0 +1,30 @@ +using SVSim.Database.Enums; +using SVSim.Database.Models; +using SVSim.Database.Services; + +namespace SVSim.Database.Services.Inventory; + +/// +/// Scoped builder returned by . Queue spend + +/// grant operations; commit to save and assemble the . +/// +/// Dispose without committing rolls back the underlying DB transaction and detaches any +/// in-memory mutations. Always wrap in await using. +/// +/// +public interface IInventoryTransaction : IAsyncDisposable +{ + Viewer Viewer { get; } + bool IsFreeplay { get; } + + Task TrySpendAsync(SpendCurrency currency, long cost, CancellationToken ct = default); + Task TryDebitAsync(UserGoodsType type, long detailId, int num, CancellationToken ct = default); + Task> GrantAsync(UserGoodsType type, long detailId, int num, CancellationToken ct = default); + Task BackfillCardCosmeticsAsync(CancellationToken ct = default); + + long EffectiveBalance(SpendCurrency currency); + bool OwnsCard(long cardId); + bool OwnsCosmetic(CosmeticType type, int id); + + Task CommitAsync(CancellationToken ct = default); +} diff --git a/SVSim.Database/Services/Inventory/InventoryCatalogException.cs b/SVSim.Database/Services/Inventory/InventoryCatalogException.cs new file mode 100644 index 0000000..d01943f --- /dev/null +++ b/SVSim.Database/Services/Inventory/InventoryCatalogException.cs @@ -0,0 +1,10 @@ +namespace SVSim.Database.Services.Inventory; + +/// +/// 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. +/// +public sealed class InventoryCatalogException : Exception +{ + public InventoryCatalogException(string message) : base(message) { } +} diff --git a/SVSim.Database/Services/Inventory/InventoryCommitResult.cs b/SVSim.Database/Services/Inventory/InventoryCommitResult.cs new file mode 100644 index 0000000..e058901 --- /dev/null +++ b/SVSim.Database/Services/Inventory/InventoryCommitResult.cs @@ -0,0 +1,20 @@ +using SVSim.Database.Services; + +namespace SVSim.Database.Services.Inventory; + +/// +/// Result of . +/// +/// — 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 reward_list fields. +/// +/// +/// — verbatim ordered (type, id, num) sequence the caller queued. No +/// collapse, no cosmetic-cascade entries. Use this for BP achieved_info and Story +/// story_reward_list popups. +/// +/// +public sealed record InventoryCommitResult( + IReadOnlyList RewardList, + IReadOnlyList Deltas); diff --git a/SVSim.Database/Services/Inventory/InventoryLoadConfig.cs b/SVSim.Database/Services/Inventory/InventoryLoadConfig.cs new file mode 100644 index 0000000..b11997c --- /dev/null +++ b/SVSim.Database/Services/Inventory/InventoryLoadConfig.cs @@ -0,0 +1,31 @@ +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Query; +using SVSim.Database.Models; + +namespace SVSim.Database.Services.Inventory; + +/// +/// Caller-supplied extra .Include chains on top of the canonical viewer-inventory query +/// in . Use to bring in extra collections needed by +/// the calling controller (e.g. MissionData, BuildDeckPurchases). +/// +public sealed class InventoryLoadConfig +{ + internal List, IQueryable>> Includes { get; } = new(); + + public InventoryLoadConfig WithInclude( + Expression> path) + { + Includes.Add(q => q.Include(path)); + return this; + } + + public InventoryLoadConfig WithInclude( + Expression>> collectionPath, + Expression> thenPath) + { + Includes.Add(q => q.Include(collectionPath).ThenInclude(thenPath)); + return this; + } +}