using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; namespace QuestionsApi.Models; public static class CreditEconomy { public const int RollingEarningLimit = 30; 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 int FirstCorrectAmount(int difficulty) => difficulty >= 5 ? 3 : difficulty >= 4 ? 2 : 1; public static int ReviewAmount(bool delayedRecovery) => delayedRecovery ? 3 : 1; } 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 int CoinAmount { get; set; } = 1; public DateTime CreatedUtc { get; set; } } public static class CreditReasonCodes { public const string FirstCorrect = "qbank_first_correct"; public const string DifficultCorrect = "qbank_difficult_correct"; public const string ScheduledReview = "scheduled_review_correct"; public const string DelayedRecovery = "delayed_recovery"; public const string MasteryPracticing = "mastery_practicing"; public const string MasteryStrong = "mastery_strong"; public const string MasteryMastered = "mastery_mastered"; public const string ExamSimulationComplete = "exam_simulation_complete"; public const string SessionGoalComplete = "session_goal_complete"; public const string DailyMissionComplete = "daily_mission_complete"; public const string WeeklyGoalComplete = "weekly_goal_complete"; public const string Purchase = "purchase"; } public sealed class CreditLedgerEntry { [BsonId] public string Id { get; set; } = ""; public string UserId { get; set; } = ""; public string OperationId { get; set; } = ""; public int RequestedAmount { get; set; } public int Amount { get; set; } public string ReasonCode { get; set; } = ""; public string Description { get; set; } = ""; public string SourceType { get; set; } = ""; public string SourceId { get; set; } = ""; public bool SubjectToRollingCap { get; set; } public string State { get; set; } = "pending"; public DateTime CreatedUtc { get; set; } public DateTime? AppliedUtc { get; set; } } public sealed class CreditAward { public int Amount { get; set; } public string ReasonCode { get; set; } = ""; public string Description { get; set; } = ""; } public sealed class CreditAwardResult { public CreditAward? Award { get; set; } public CreditStatusResponse Status { get; set; } = new(); } public sealed class CreditLedgerPage { public List Items { get; set; } = []; public DateTime? NextBefore { get; set; } } public sealed class CreditLedgerEntryResponse { public int Amount { get; set; } public string ReasonCode { get; set; } = ""; public string Description { 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 bool MasteryAwardBaselineApplied { 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 record CreditAwardRequest( string OperationId, int Amount, string ReasonCode, string Description, string SourceType, string SourceId, bool SubjectToRollingCap = true); 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(); }