using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; namespace QuestionsApi.Models; public static class CreditEconomy { public const int RollingEarningLimit = 20; public static readonly TimeSpan EarningWindow = TimeSpan.FromHours(24); public const string ExamTokenSku = "exam_token_1"; public const string QBankResetTokenSku = "qbank_reset_1"; public const int ExamTokenPrice = 100; public const int QBankResetTokenPrice = 300; public static readonly IReadOnlyList Products = [ new() { Sku = ExamTokenSku, Name = "Exam Simulation Token", Description = "Starts one new full exam simulation.", CoinPrice = ExamTokenPrice, Grant = RzStoreGrant.ExamToken }, new() { Sku = QBankResetTokenSku, Name = "QBank Reset Token", Description = "Makes every QBank question eligible again without erasing progress.", CoinPrice = QBankResetTokenPrice, Grant = RzStoreGrant.QBankResetToken } ]; public static RzStoreProduct? FindProduct(string sku) => Products.FirstOrDefault(value => value.Sku.Equals(sku, StringComparison.Ordinal)); } public static class SlotMachineEconomy { public const int SpinCost = 0; // TODO: restore to 50 once testing is done public const int RollingSpinLimit = 200; public static readonly TimeSpan SpinWindow = TimeSpan.FromHours(24); public static readonly IReadOnlyList Rewards = [ new() { Key = "jackpot", Label = "10,000 RZ Coins", Description = "Jackpot!", Grant = RzStoreGrant.Coins, CoinAmount = 10000, Probability = 0.005 }, new() { Key = "epic", Label = "2,000 RZ Coins", Description = "Epic Reward", Grant = RzStoreGrant.Coins, CoinAmount = 2000, Probability = 0.02 }, new() { Key = "great", Label = "500 RZ Coins", Description = "Great Reward", Grant = RzStoreGrant.Coins, CoinAmount = 500, Probability = 0.10 }, new() { Key = "exam_token", Label = "Exam Simulation Token", Description = "Starts one new full exam simulation.", Grant = RzStoreGrant.ExamToken, Probability = 0.15 }, new() { Key = "qbank_reset", Label = "QBank Reset Token", Description = "Makes every QBank question eligible again.", Grant = RzStoreGrant.QBankResetToken, Probability = 0.20 }, new() { Key = "nice", Label = "100 RZ Coins", Description = "Nice!", Grant = RzStoreGrant.Coins, CoinAmount = 100, Probability = 0.30 }, new() { Key = "good_luck", Label = "50 RZ Coins", Description = "Good luck!", Grant = RzStoreGrant.Coins, CoinAmount = 50, Probability = 0.225 }, ]; public static SpinReward RollReward(double roll) { var cumulative = 0.0; foreach (var reward in Rewards) { cumulative += reward.Probability; if (roll < cumulative) return reward; } return Rewards[^1]; } public static SpinReward? FindReward(string key) => Rewards.FirstOrDefault(value => value.Key.Equals(key, StringComparison.Ordinal)); } public enum RzStoreGrant { ExamToken, QBankResetToken, Coins } public sealed class SpinReward { public string Key { get; set; } = ""; public string Label { get; set; } = ""; public string Description { get; set; } = ""; public RzStoreGrant Grant { get; set; } public int CoinAmount { get; set; } public double Probability { get; set; } } public sealed class RzStoreProduct { public string Sku { get; set; } = ""; public string Name { get; set; } = ""; public string Description { get; set; } = ""; public int CoinPrice { get; set; } public RzStoreGrant Grant { get; set; } } public sealed class CreditEvent { public string OperationId { get; set; } = ""; public DateTime CreatedUtc { get; set; } } public sealed class CreditPurchaseEvent { public string OperationId { get; set; } = ""; public string Sku { get; set; } = ""; public int CoinCost { get; set; } public DateTime CreatedUtc { get; set; } } public sealed class SpinEvent { public string OperationId { get; set; } = ""; public string RewardKey { get; set; } = ""; public int CoinsWon { get; set; } public DateTime CreatedUtc { get; set; } } public sealed class UserCreditAccount { [BsonId] [BsonRepresentation(BsonType.ObjectId)] public string? Id { get; set; } public string UserId { get; set; } = ""; public int Credits { get; set; } public int ExamTokens { get; set; } public int QBankResetTokens { get; set; } public int BankCycle { get; set; } public bool InitialExamGrantApplied { get; set; } public List RecentEarnings { get; set; } = []; public List Purchases { get; set; } = []; public List Spins { get; set; } = []; public List RecentSpins { get; set; } = []; public List AppliedOperations { get; set; } = []; public DateTime UpdatedUtc { get; set; } = DateTime.UtcNow; } public sealed class CreditStatusResponse { public int Credits { get; set; } public int EarnedInRollingWindow { get; set; } public int RemainingEarningCapacity { get; set; } public int RollingWindowHours { get; set; } = 24; public int ExamTokens { get; set; } public int QBankResetTokens { get; set; } public int BankCycle { get; set; } public IReadOnlyList Products { get; set; } = CreditEconomy.Products; } public sealed class CreditPurchaseRequest { public string Sku { get; set; } = ""; public string OperationId { get; set; } = ""; } public sealed class QBankResetRequest { public string Confirmation { get; set; } = ""; public string OperationId { get; set; } = ""; } public sealed class SpinRequest { public string OperationId { get; set; } = ""; } public enum CreditOperationResult { Succeeded, InsufficientCredits, InsufficientTokens, OperationConflict, RateLimited } public sealed class CreditOperationResponse { public CreditOperationResult Result { get; set; } public CreditStatusResponse Status { get; set; } = new(); } public sealed class SpinConfigResponse { public int SpinCost { get; set; } = SlotMachineEconomy.SpinCost; public IReadOnlyList Rewards { get; set; } = SlotMachineEconomy.Rewards; } public sealed class SpinResponse { public CreditOperationResult Result { get; set; } public string RewardKey { get; set; } = ""; public string RewardLabel { get; set; } = ""; public int CoinsWon { get; set; } public CreditStatusResponse Status { get; set; } = new(); }