diff --git a/src/Billing/Services/Implementations/CheckoutSessionCompletedHandler.cs b/src/Billing/Services/Implementations/CheckoutSessionCompletedHandler.cs index d0081daa026f..10f375b9fe6b 100644 --- a/src/Billing/Services/Implementations/CheckoutSessionCompletedHandler.cs +++ b/src/Billing/Services/Implementations/CheckoutSessionCompletedHandler.cs @@ -64,7 +64,9 @@ public async Task HandleAsync(Event parsedEvent) user.PremiumExpirationDate = subscription.GetCurrentPeriodEnd(); user.MaxStorageGb = (short)premiumPlan.Storage.Provided; user.LicenseKey = string.IsNullOrWhiteSpace(user.LicenseKey) ? CoreHelpers.SecureRandomString(20) : user.LicenseKey; - user.RevisionDate = DateTime.UtcNow; + // Bump AccountRevisionDate so clients' revision-gated syncs pick up the + // premium change even when the one-time push notification is missed. + user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow; await userRepository.ReplaceAsync(user); await pushNotificationAdapter.NotifyPremiumStatusChangedAsync(user); diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 446853583f67..fa5d2c8a3676 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -822,7 +822,9 @@ private async Task EnablePremiumAsync(User user, DateTime? expirationDate) { user.Premium = true; user.PremiumExpirationDate = expirationDate; - user.RevisionDate = DateTime.UtcNow; + // Bump AccountRevisionDate so clients' revision-gated syncs pick up the + // premium change even when the one-time push notification is missed. + user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow; await _userRepository.ReplaceAsync(user); } } @@ -839,7 +841,9 @@ private async Task DisablePremiumAsync(User user, DateTime? expirationDate) { user.Premium = false; user.PremiumExpirationDate = expirationDate; - user.RevisionDate = DateTime.UtcNow; + // Bump AccountRevisionDate so clients' revision-gated syncs pick up the + // premium change even when the one-time push notification is missed. + user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow; await _userRepository.ReplaceAsync(user); } } diff --git a/test/Core.Test/Services/UserServiceTests.cs b/test/Core.Test/Services/UserServiceTests.cs index af2544ee2443..52c789a7f32b 100644 --- a/test/Core.Test/Services/UserServiceTests.cs +++ b/test/Core.Test/Services/UserServiceTests.cs @@ -47,6 +47,41 @@ public async Task SaveUserAsync_SetsNameToNull_WhenNameIsEmpty(SutProvider sutProvider, User user) + { + user.Premium = false; + user.Gateway = GatewayType.Stripe; + var staleAccountRevisionDate = user.AccountRevisionDate = DateTime.UtcNow.AddDays(-1); + var expirationDate = DateTime.UtcNow.AddDays(30); + sutProvider.GetDependency().GetByIdAsync(user.Id).Returns(user); + + await sutProvider.Sut.EnablePremiumAsync(user.Id, expirationDate); + + await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is(u => + u.Premium && + u.PremiumExpirationDate == expirationDate && + u.AccountRevisionDate > staleAccountRevisionDate)); + } + + [Theory, BitAutoData] + public async Task DisablePremiumAsync_UserPremium_DisablesPremiumAndBumpsAccountRevisionDate( + SutProvider sutProvider, User user) + { + user.Premium = true; + var staleAccountRevisionDate = user.AccountRevisionDate = DateTime.UtcNow.AddDays(-1); + var expirationDate = DateTime.UtcNow.AddDays(7); + sutProvider.GetDependency().GetByIdAsync(user.Id).Returns(user); + + await sutProvider.Sut.DisablePremiumAsync(user.Id, expirationDate); + + await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is(u => + !u.Premium && + u.PremiumExpirationDate == expirationDate && + u.AccountRevisionDate > staleAccountRevisionDate)); + } + [Theory, BitAutoData] public async Task UpdateLicenseAsync_Success(SutProvider sutProvider, User user, UserLicense userLicense)