From b4f69929183bd88d130c3a316c9b1aa5d0a49d45 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Fri, 29 May 2026 13:27:22 -0400 Subject: [PATCH] feat(services): declare entitlements + currency-spend primitives Co-Authored-By: Claude Sonnet 4.6 --- .../Services/ICurrencySpendService.cs | 14 ++++++ .../Services/IViewerEntitlements.cs | 46 +++++++++++++++++++ SVSim.Database/Services/SpendCurrency.cs | 16 +++++++ 3 files changed, 76 insertions(+) create mode 100644 SVSim.Database/Services/ICurrencySpendService.cs create mode 100644 SVSim.Database/Services/IViewerEntitlements.cs create mode 100644 SVSim.Database/Services/SpendCurrency.cs diff --git a/SVSim.Database/Services/ICurrencySpendService.cs b/SVSim.Database/Services/ICurrencySpendService.cs new file mode 100644 index 0000000..6aa27c1 --- /dev/null +++ b/SVSim.Database/Services/ICurrencySpendService.cs @@ -0,0 +1,14 @@ +using SVSim.Database.Models; + +namespace SVSim.Database.Services; + +/// +/// Centralized debit primitive — the symmetric twin of RewardGrantService.ApplyAsync. +/// Encapsulates the affordability-check + deduction + post-state-total pattern that was inlined +/// across the shop/pack controllers. Does NOT call SaveChangesAsync; the caller saves. +/// Freeplay (for Crystal/Rupee/RedEther) makes spends always succeed without deducting. +/// +public interface ICurrencySpendService +{ + Task TrySpendAsync(Viewer viewer, SpendCurrency currency, long cost, CancellationToken ct = default); +} diff --git a/SVSim.Database/Services/IViewerEntitlements.cs b/SVSim.Database/Services/IViewerEntitlements.cs new file mode 100644 index 0000000..1a3ba83 --- /dev/null +++ b/SVSim.Database/Services/IViewerEntitlements.cs @@ -0,0 +1,46 @@ +using SVSim.Database.Enums; +using SVSim.Database.Models; + +namespace SVSim.Database.Services; + +/// +/// The single read/ownership authority for what a viewer is *treated as* owning. Knows the +/// Freeplay flag; all freeplay read-side behavior lives here. See +/// docs/superpowers/specs/2026-05-29-freeplay-mode-design.md. +/// +public interface IViewerEntitlements +{ + /// True when the global Freeplay config section is enabled. + bool IsFreeplay { get; } + + /// + /// The balance the viewer is treated as having: the configured freeplay amount for + /// Crystal/Rupee/RedEther when freeplay is on, otherwise (and always for SpotPoint) the real + /// viewer.Currency field. + /// + long EffectiveBalance(Viewer viewer, SpendCurrency currency); + + bool OwnsCard(Viewer viewer, long cardId); + + /// uses (Skin == leader skin). + bool OwnsCosmetic(Viewer viewer, CosmeticType type, int id); + + /// The full owned-card projection for /load/index's user_card_list. + Task> EffectiveOwnedCardsAsync(Viewer viewer, CancellationToken ct = default); + + /// The cosmetic id-lists + leader-skin catalog/owned-set for /load/index. + Task EffectiveCosmeticsAsync(Viewer viewer, CancellationToken ct = default); +} + +/// +/// Cosmetic projection bundle for /load/index. The four id-lists are "what the viewer owns" +/// (all of them in freeplay). Leader skins are always the full catalog with a per-skin owned flag; +/// is every skin id in freeplay. +/// +public sealed record EffectiveCosmetics( + IReadOnlyList SleeveIds, + IReadOnlyList EmblemIds, + IReadOnlyList DegreeIds, + IReadOnlyList MyPageBackgroundIds, + IReadOnlyList AllLeaderSkins, + IReadOnlySet OwnedLeaderSkinIds); diff --git a/SVSim.Database/Services/SpendCurrency.cs b/SVSim.Database/Services/SpendCurrency.cs new file mode 100644 index 0000000..ee7f246 --- /dev/null +++ b/SVSim.Database/Services/SpendCurrency.cs @@ -0,0 +1,16 @@ +namespace SVSim.Database.Services; + +/// The scalar wallet currencies the central debit primitive understands. +public enum SpendCurrency { Crystal, Rupee, RedEther, SpotPoint } + +public enum SpendOutcome { Success, Insufficient } + +/// +/// Result of a call. +/// is the balance the client should show after the spend — the real post-deduction balance, or the +/// freeplay effective balance when the spend was a freeplay no-op. +/// +public sealed record SpendResult(SpendOutcome Outcome, long PostStateTotal) +{ + public bool Success => Outcome == SpendOutcome.Success; +}