diff --git a/src/Billing/Controllers/StripeController.cs b/src/Billing/Controllers/StripeController.cs index 7b60a47b9..092cc9333 100644 --- a/src/Billing/Controllers/StripeController.cs +++ b/src/Billing/Controllers/StripeController.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Stripe; using System; +using System.Collections.Generic; using System.Threading.Tasks; namespace Bit.Billing.Controllers @@ -36,7 +37,7 @@ namespace Bit.Billing.Controllers return new BadRequestResult(); } - var parsedEvent = StripeEventUtility.ParseEventDataItem(body); + StripeEvent parsedEvent = StripeEventUtility.ParseEventDataItem(body); if(string.IsNullOrWhiteSpace(parsedEvent?.Id)) { return new BadRequestResult(); @@ -47,30 +48,57 @@ namespace Bit.Billing.Controllers return new BadRequestResult(); } - if(parsedEvent.Type == "customer.subscription.deleted") + if(parsedEvent.Type.Equals("customer.subscription.deleted") || + parsedEvent.Type.Equals("customer.subscription.updated")) { - var subscription = Mapper.MapFromJson(parsedEvent.Data.Object.ToString()) - as StripeSubscription; - if(subscription?.Status == "canceled") + StripeSubscription subscription = Mapper.MapFromJson(parsedEvent.Data.Object.ToString()); + var ids = GetIdsFromMetaData(subscription.Metadata); + + if(parsedEvent.Type.Equals("customer.subscription.deleted") && subscription?.Status == "canceled") { - if(subscription.Metadata?.ContainsKey("organizationId") ?? false) + // org + if(ids.Item1.HasValue) { - var orgIdGuid = new Guid(subscription.Metadata["organizationId"]); - await _organizationService.DisableAsync(orgIdGuid); + await _organizationService.DisableAsync(ids.Item1.Value, subscription.CurrentPeriodEnd); } - else if(subscription.Metadata?.ContainsKey("userId") ?? false) + // user + else if(ids.Item2.HasValue) { - var userIdGuid = new Guid(subscription.Metadata["userId"]); - await _userService.DisablePremiumAsync(userIdGuid); + await _userService.DisablePremiumAsync(ids.Item2.Value, subscription.CurrentPeriodEnd); + } + } + else if(parsedEvent.Type.Equals("customer.subscription.updated")) + { + // org + if(ids.Item1.HasValue) + { + await _organizationService.UpdateExpirationDateAsync(ids.Item1.Value, subscription.CurrentPeriodEnd); + } + // user + else if(ids.Item2.HasValue) + { + await _userService.UpdatePremiumExpirationAsync(ids.Item2.Value, subscription.CurrentPeriodEnd); } } - } - else - { - // Not handling this event type. } return new OkResult(); } + + private Tuple GetIdsFromMetaData(IDictionary metaData) + { + Guid? orgId = null; + Guid? userId = null; + if(metaData?.ContainsKey("organizationId") ?? false) + { + orgId = new Guid(metaData["organizationId"]); + } + else if(metaData?.ContainsKey("userId") ?? false) + { + userId = new Guid(metaData["userId"]); + } + + return new Tuple(orgId, userId); + } } } diff --git a/src/Core/Services/IOrganizationService.cs b/src/Core/Services/IOrganizationService.cs index cd560cae5..8bcf5f650 100644 --- a/src/Core/Services/IOrganizationService.cs +++ b/src/Core/Services/IOrganizationService.cs @@ -18,7 +18,8 @@ namespace Bit.Core.Services Task AdjustSeatsAsync(Guid organizationId, int seatAdjustment); Task> SignUpAsync(OrganizationSignup organizationSignup); Task DeleteAsync(Organization organization); - Task DisableAsync(Guid organizationId); + Task DisableAsync(Guid organizationId, DateTime? expirationDate); + Task UpdateExpirationDateAsync(Guid organizationId, DateTime? expirationDate); Task EnableAsync(Guid organizationId); Task UpdateAsync(Organization organization, bool updateBilling = false); Task InviteUserAsync(Guid organizationId, Guid invitingUserId, string email, diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index a51f04199..fc998e86c 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -45,6 +45,7 @@ namespace Bit.Core.Services Task ReplacePaymentMethodAsync(User user, string paymentToken); Task CancelPremiumAsync(User user, bool endOfPeriod = false); Task ReinstatePremiumAsync(User user); - Task DisablePremiumAsync(Guid userId); + Task DisablePremiumAsync(Guid userId, DateTime? expirationDate); + Task UpdatePremiumExpirationAsync(Guid userId, DateTime? expirationDate); } } diff --git a/src/Core/Services/Implementations/BraintreePaymentService.cs b/src/Core/Services/Implementations/BraintreePaymentService.cs index 4edcc59e6..b17383913 100644 --- a/src/Core/Services/Implementations/BraintreePaymentService.cs +++ b/src/Core/Services/Implementations/BraintreePaymentService.cs @@ -243,6 +243,8 @@ namespace Bit.Core.Services user.Gateway = Enums.GatewayType.Braintree; user.GatewayCustomerId = customerResult.Target.Id; user.GatewaySubscriptionId = subResult.Target.Id; + user.Premium = true; + user.PremiumExpirationDate = subResult.Target.BillingPeriodEndDate; } public async Task ReinstateSubscriptionAsync(ISubscriber subscriber) diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs index 21c763dbf..3af351a48 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -527,18 +527,31 @@ namespace Bit.Core.Services await _organizationRepository.DeleteAsync(organization); } - public async Task DisableAsync(Guid organizationId) + public async Task DisableAsync(Guid organizationId, DateTime? expirationDate) { var org = await _organizationRepository.GetByIdAsync(organizationId); if(org != null && org.Enabled) { org.Enabled = false; + org.ExpirationDate = expirationDate; + org.RevisionDate = DateTime.UtcNow; await _organizationRepository.ReplaceAsync(org); // TODO: send email to owners? } } + public async Task UpdateExpirationDateAsync(Guid organizationId, DateTime? expirationDate) + { + var org = await _organizationRepository.GetByIdAsync(organizationId); + if(org != null) + { + org.ExpirationDate = expirationDate; + org.RevisionDate = DateTime.UtcNow; + await _organizationRepository.ReplaceAsync(org); + } + } + public async Task EnableAsync(Guid organizationId) { var org = await _organizationRepository.GetByIdAsync(organizationId); diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index 166ed01d8..2194b2d2e 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -63,6 +63,8 @@ namespace Bit.Core.Services user.Gateway = Enums.GatewayType.Stripe; user.GatewayCustomerId = customer.Id; user.GatewaySubscriptionId = subscription.Id; + user.Premium = true; + user.PremiumExpirationDate = subscription.CurrentPeriodEnd; } public async Task AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage, diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 053ace324..5813f36af 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -574,6 +574,7 @@ namespace Bit.Core.Services { user.MaxStorageGb = 10240; // 10 TB user.LicenseKey = license.LicenseKey; + user.PremiumExpirationDate = license.Expires; } else { @@ -640,12 +641,24 @@ namespace Bit.Core.Services await paymentService.ReinstateSubscriptionAsync(user); } - public async Task DisablePremiumAsync(Guid userId) + public async Task DisablePremiumAsync(Guid userId, DateTime? expirationDate) { var user = await _userRepository.GetByIdAsync(userId); if(user != null && user.Premium) { user.Premium = false; + user.PremiumExpirationDate = expirationDate; + user.RevisionDate = DateTime.UtcNow; + await _userRepository.ReplaceAsync(user); + } + } + + public async Task UpdatePremiumExpirationAsync(Guid userId, DateTime? expirationDate) + { + var user = await _userRepository.GetByIdAsync(userId); + if(user != null) + { + user.PremiumExpirationDate = expirationDate; user.RevisionDate = DateTime.UtcNow; await _userRepository.ReplaceAsync(user); }