feat(services): declare entitlements + currency-spend primitives
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
14
SVSim.Database/Services/ICurrencySpendService.cs
Normal file
14
SVSim.Database/Services/ICurrencySpendService.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using SVSim.Database.Models;
|
||||
|
||||
namespace SVSim.Database.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Centralized debit primitive — the symmetric twin of <c>RewardGrantService.ApplyAsync</c>.
|
||||
/// Encapsulates the affordability-check + deduction + post-state-total pattern that was inlined
|
||||
/// across the shop/pack controllers. Does NOT call <c>SaveChangesAsync</c>; the caller saves.
|
||||
/// Freeplay (for Crystal/Rupee/RedEther) makes spends always succeed without deducting.
|
||||
/// </summary>
|
||||
public interface ICurrencySpendService
|
||||
{
|
||||
Task<SpendResult> TrySpendAsync(Viewer viewer, SpendCurrency currency, long cost, CancellationToken ct = default);
|
||||
}
|
||||
46
SVSim.Database/Services/IViewerEntitlements.cs
Normal file
46
SVSim.Database/Services/IViewerEntitlements.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using SVSim.Database.Enums;
|
||||
using SVSim.Database.Models;
|
||||
|
||||
namespace SVSim.Database.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public interface IViewerEntitlements
|
||||
{
|
||||
/// <summary>True when the global Freeplay config section is enabled.</summary>
|
||||
bool IsFreeplay { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// <c>viewer.Currency</c> field.
|
||||
/// </summary>
|
||||
long EffectiveBalance(Viewer viewer, SpendCurrency currency);
|
||||
|
||||
bool OwnsCard(Viewer viewer, long cardId);
|
||||
|
||||
/// <summary><paramref name="type"/> uses <see cref="CosmeticType"/> (Skin == leader skin).</summary>
|
||||
bool OwnsCosmetic(Viewer viewer, CosmeticType type, int id);
|
||||
|
||||
/// <summary>The full owned-card projection for /load/index's user_card_list.</summary>
|
||||
Task<IReadOnlyList<OwnedCardEntry>> EffectiveOwnedCardsAsync(Viewer viewer, CancellationToken ct = default);
|
||||
|
||||
/// <summary>The cosmetic id-lists + leader-skin catalog/owned-set for /load/index.</summary>
|
||||
Task<EffectiveCosmetics> EffectiveCosmeticsAsync(Viewer viewer, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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;
|
||||
/// <see cref="OwnedLeaderSkinIds"/> is every skin id in freeplay.
|
||||
/// </summary>
|
||||
public sealed record EffectiveCosmetics(
|
||||
IReadOnlyList<int> SleeveIds,
|
||||
IReadOnlyList<int> EmblemIds,
|
||||
IReadOnlyList<int> DegreeIds,
|
||||
IReadOnlyList<int> MyPageBackgroundIds,
|
||||
IReadOnlyList<LeaderSkinEntry> AllLeaderSkins,
|
||||
IReadOnlySet<int> OwnedLeaderSkinIds);
|
||||
16
SVSim.Database/Services/SpendCurrency.cs
Normal file
16
SVSim.Database/Services/SpendCurrency.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace SVSim.Database.Services;
|
||||
|
||||
/// <summary>The scalar wallet currencies the central debit primitive understands.</summary>
|
||||
public enum SpendCurrency { Crystal, Rupee, RedEther, SpotPoint }
|
||||
|
||||
public enum SpendOutcome { Success, Insufficient }
|
||||
|
||||
/// <summary>
|
||||
/// Result of a <see cref="ICurrencySpendService.TrySpendAsync"/> call. <see cref="PostStateTotal"/>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public sealed record SpendResult(SpendOutcome Outcome, long PostStateTotal)
|
||||
{
|
||||
public bool Success => Outcome == SpendOutcome.Success;
|
||||
}
|
||||
Reference in New Issue
Block a user