using MongoDB.Driver; using QuestionsApi.Models; namespace QuestionsApi.Services; public sealed class DonationStore { private readonly IMongoCollection _donations; public DonationStore(MongoDatabaseProvider mongo) { _donations = mongo.Database.GetCollection("Donations"); _donations.Indexes.CreateMany([ new CreateIndexModel(Builders.IndexKeys.Ascending(value => value.UserId).Descending(value => value.CreatedUtc)), new CreateIndexModel(Builders.IndexKeys.Ascending(value => value.StripeCheckoutSessionId), new CreateIndexOptions { Unique = true, PartialFilterExpression = Builders.Filter.Type(value => value.StripeCheckoutSessionId, MongoDB.Bson.BsonType.String) }), new CreateIndexModel(Builders.IndexKeys.Ascending(value => value.Status).Descending(value => value.CreatedUtc)) ]); } public async Task CreateAsync(string userId, long amountCents) { var donation = new Donation { UserId = userId, AmountCents = amountCents }; await _donations.InsertOneAsync(donation); return donation; } public async Task GetOwnedAsync(string id, string userId) => await _donations.Find(value => value.Id == id && value.UserId == userId).FirstOrDefaultAsync(); public async Task AttachCheckoutSessionAsync(string id, string sessionId) => await _donations.UpdateOneAsync( value => value.Id == id && value.Status == DonationStatus.Pending, Builders.Update.Set(value => value.StripeCheckoutSessionId, sessionId).Set(value => value.UpdatedUtc, DateTime.UtcNow)); public async Task MarkCheckoutCreationFailedAsync(string id, string failureCode) => await _donations.UpdateOneAsync( value => value.Id == id && value.Status == DonationStatus.Pending, Builders.Update.Set(value => value.Status, DonationStatus.Failed) .Set(value => value.FailureCode, failureCode).Set(value => value.UpdatedUtc, DateTime.UtcNow)); public async Task ApplyStripeStatusAsync( string id, string sessionId, long amountCents, string currency, string status, string paymentIntentId, string eventId) { if (string.IsNullOrWhiteSpace(id) || string.IsNullOrWhiteSpace(sessionId)) return false; var filter = Builders.Filter.Eq(value => value.Id, id) & Builders.Filter.Eq(value => value.AmountCents, amountCents) & Builders.Filter.Eq(value => value.Currency, currency.ToLowerInvariant()) & (Builders.Filter.Eq(value => value.StripeCheckoutSessionId, sessionId) | Builders.Filter.Eq(value => value.StripeCheckoutSessionId, null)); if (status != DonationStatus.Paid) filter &= Builders.Filter.Eq(value => value.Status, DonationStatus.Pending); var now = DateTime.UtcNow; var update = Builders.Update.Set(value => value.Status, status) .Set(value => value.StripeCheckoutSessionId, sessionId) .Set(value => value.StripePaymentIntentId, paymentIntentId) .Set(value => value.LastStripeEventId, eventId) .Set(value => value.UpdatedUtc, now); if (status == DonationStatus.Paid) update = update.Set(value => value.PaidUtc, now); return (await _donations.UpdateOneAsync(filter, update)).ModifiedCount > 0; } }