using MongoDB.Driver; using QuestionsApi.Models; namespace QuestionsApi.Services; public sealed class AccessPurchaseStore { private readonly IMongoCollection _purchases; public AccessPurchaseStore(MongoDatabaseProvider mongo) { _purchases = mongo.Database.GetCollection("AccessPurchases"); _purchases.Indexes.CreateMany([ new CreateIndexModel( Builders.IndexKeys.Ascending(value => value.UserId).Descending(value => value.CreatedUtc)), new CreateIndexModel( Builders.IndexKeys.Ascending(value => value.ProviderSessionId), new CreateIndexOptions { Unique = true, PartialFilterExpression = Builders.Filter.Ne(value => value.ProviderSessionId, "") }) ]); } public async Task CreateAsync(string userId, AccessProduct product) { var purchase = new AccessPurchase { UserId = userId, Sku = product.Sku, AmountCents = product.AmountCents, Currency = product.Currency }; await _purchases.InsertOneAsync(purchase); return purchase; } public async Task GetOwnedAsync(string id, string userId) => await _purchases.Find(value => value.Id == id && value.UserId == userId).FirstOrDefaultAsync(); public async Task GetAsync(string id) => await _purchases.Find(value => value.Id == id).FirstOrDefaultAsync(); public Task AttachSessionAsync(string id, string sessionId) => _purchases.UpdateOneAsync( value => value.Id == id && value.Status == AccessPurchaseStatus.Pending, Builders.Update .Set(value => value.ProviderSessionId, sessionId) .Set(value => value.UpdatedUtc, DateTime.UtcNow)); public Task MarkCheckoutFailedAsync(string id) => _purchases.UpdateOneAsync( value => value.Id == id && value.Status == AccessPurchaseStatus.Pending, Builders.Update .Set(value => value.Status, AccessPurchaseStatus.Failed) .Set(value => value.UpdatedUtc, DateTime.UtcNow)); public async Task ApplyProviderStatusAsync( string id, string sessionId, long amountCents, string currency, string status, string paymentId, string eventId) { if (string.IsNullOrWhiteSpace(id) || string.IsNullOrWhiteSpace(sessionId)) return null; var filters = Builders.Filter; var filter = filters.Eq(value => value.Id, id) & filters.Eq(value => value.AmountCents, amountCents) & filters.Eq(value => value.Currency, currency.ToLowerInvariant()) & (filters.Eq(value => value.ProviderSessionId, sessionId) | filters.Eq(value => value.ProviderSessionId, "")) & filters.Not(filters.AnyEq(value => value.ProviderEventIds, eventId)); var update = Builders.Update .Set(value => value.Status, status) .Set(value => value.ProviderSessionId, sessionId) .Set(value => value.ProviderPaymentId, paymentId) .AddToSet(value => value.ProviderEventIds, eventId) .Set(value => value.UpdatedUtc, DateTime.UtcNow); return await _purchases.FindOneAndUpdateAsync(filter, update, new FindOneAndUpdateOptions { ReturnDocument = ReturnDocument.After }); } public Task MarkGrantAppliedAsync(string id) => _purchases.UpdateOneAsync( value => value.Id == id && value.Status == AccessPurchaseStatus.Paid, Builders.Update .Set(value => value.GrantApplied, true) .Set(value => value.UpdatedUtc, DateTime.UtcNow)); public static AccessPurchaseStatusResponse ToStatus(AccessPurchase purchase) => new() { PurchaseId = purchase.Id ?? "", Sku = purchase.Sku, AmountCents = purchase.AmountCents, Currency = purchase.Currency, Status = purchase.Status }; }