79 lines
3.1 KiB
C#
79 lines
3.1 KiB
C#
using MongoDB.Bson;
|
|
using MongoDB.Driver;
|
|
using QuestionsApi.Models;
|
|
|
|
namespace QuestionsApi.Services;
|
|
|
|
public sealed class CreditStore
|
|
{
|
|
private readonly IMongoCollection<UserCreditAccount> _accounts;
|
|
private readonly TimeProvider _clock;
|
|
|
|
public CreditStore(MongoDatabaseProvider mongo, TimeProvider clock)
|
|
{
|
|
_accounts = mongo.Database.GetCollection<UserCreditAccount>("UserCreditAccounts");
|
|
_clock = clock;
|
|
_accounts.Indexes.CreateOne(new CreateIndexModel<UserCreditAccount>(
|
|
Builders<UserCreditAccount>.IndexKeys.Ascending(value => value.UserId),
|
|
new CreateIndexOptions { Unique = true, Name = "uq_credit_user" }));
|
|
}
|
|
|
|
public async Task<bool> AwardForFirstAnswerAsync(string userId, string attemptId)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(attemptId)) return false;
|
|
var now = _clock.GetUtcNow().UtcDateTime;
|
|
await EnsureAndCleanAsync(userId, now);
|
|
|
|
var filters = Builders<UserCreditAccount>.Filter;
|
|
var belowCap = new BsonDocumentFilterDefinition<UserCreditAccount>(
|
|
new BsonDocument("$expr", new BsonDocument("$lt", new BsonArray
|
|
{
|
|
new BsonDocument("$size", "$RecentEarnings"),
|
|
CreditEconomy.RollingEarningLimit
|
|
})));
|
|
var filter = filters.Eq(value => value.UserId, userId) &
|
|
filters.Not(filters.ElemMatch(value => value.RecentEarnings,
|
|
earning => earning.OperationId == attemptId)) &
|
|
belowCap;
|
|
var update = Builders<UserCreditAccount>.Update
|
|
.Inc(value => value.Credits, 1)
|
|
.Push(value => value.RecentEarnings, new CreditEvent
|
|
{
|
|
OperationId = attemptId,
|
|
CreatedUtc = now
|
|
})
|
|
.Set(value => value.UpdatedUtc, now);
|
|
return (await _accounts.UpdateOneAsync(filter, update)).ModifiedCount == 1;
|
|
}
|
|
|
|
public async Task<CreditStatusResponse> GetStatusAsync(string userId)
|
|
{
|
|
var now = _clock.GetUtcNow().UtcDateTime;
|
|
await EnsureAndCleanAsync(userId, now);
|
|
var account = await _accounts.Find(value => value.UserId == userId).FirstAsync();
|
|
return new CreditStatusResponse
|
|
{
|
|
Credits = account.Credits,
|
|
EarnedInRollingWindow = account.RecentEarnings.Count,
|
|
RemainingEarningCapacity = Math.Max(
|
|
0, CreditEconomy.RollingEarningLimit - account.RecentEarnings.Count)
|
|
};
|
|
}
|
|
|
|
private async Task EnsureAndCleanAsync(string userId, DateTime now)
|
|
{
|
|
await _accounts.UpdateOneAsync(
|
|
value => value.UserId == userId,
|
|
Builders<UserCreditAccount>.Update
|
|
.SetOnInsert(value => value.UserId, userId)
|
|
.SetOnInsert(value => value.UpdatedUtc, now),
|
|
new UpdateOptions { IsUpsert = true });
|
|
var cutoff = now - CreditEconomy.EarningWindow;
|
|
await _accounts.UpdateOneAsync(
|
|
value => value.UserId == userId,
|
|
Builders<UserCreditAccount>.Update.PullFilter(
|
|
value => value.RecentEarnings,
|
|
earning => earning.CreatedUtc <= cutoff));
|
|
}
|
|
}
|