Files
step3/api/QuestionsApi/Models/CreditModels.cs
T
hz 0bd74fcabf Temporarily make slot machine spins free for testing
SpinCost set to 0 so spins don't require RZ Coins - revert to 50
once testing is done (marked with a TODO). The frontend already
reads spin cost from GET /api/credits/spin-config rather than
hardcoding it, so the "Cost per spin" display and the Spin button's
enabled state update automatically.
2026-07-30 03:39:44 -05:00

184 lines
6.5 KiB
C#

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<RzStoreProduct> 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<SpinReward> 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<CreditEvent> RecentEarnings { get; set; } = [];
public List<CreditPurchaseEvent> Purchases { get; set; } = [];
public List<SpinEvent> Spins { get; set; } = [];
public List<CreditEvent> RecentSpins { get; set; } = [];
public List<string> 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<RzStoreProduct> 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<SpinReward> 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();
}