using Bit.Billing.Constants; using Bit.Billing.Services; using Bit.Billing.Services.Implementations; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models.StaticStore.Plans; using Bit.Core.Billing.Pricing; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Services; using Newtonsoft.Json.Linq; using NSubstitute; using Quartz; using Stripe; using Xunit; using Event = Stripe.Event; namespace Bit.Billing.Test.Services; public class SubscriptionUpdatedHandlerTests { private readonly IStripeEventService _stripeEventService; private readonly IStripeEventUtilityService _stripeEventUtilityService; private readonly IOrganizationService _organizationService; private readonly IStripeFacade _stripeFacade; private readonly IOrganizationSponsorshipRenewCommand _organizationSponsorshipRenewCommand; private readonly IUserService _userService; private readonly IPushNotificationService _pushNotificationService; private readonly IOrganizationRepository _organizationRepository; private readonly ISchedulerFactory _schedulerFactory; private readonly IOrganizationEnableCommand _organizationEnableCommand; private readonly IOrganizationDisableCommand _organizationDisableCommand; private readonly IPricingClient _pricingClient; private readonly IScheduler _scheduler; private readonly SubscriptionUpdatedHandler _sut; public SubscriptionUpdatedHandlerTests() { _stripeEventService = Substitute.For(); _stripeEventUtilityService = Substitute.For(); _organizationService = Substitute.For(); _stripeFacade = Substitute.For(); _organizationSponsorshipRenewCommand = Substitute.For(); _userService = Substitute.For(); _pushNotificationService = Substitute.For(); _organizationRepository = Substitute.For(); _schedulerFactory = Substitute.For(); _organizationEnableCommand = Substitute.For(); _organizationDisableCommand = Substitute.For(); _pricingClient = Substitute.For(); _scheduler = Substitute.For(); _schedulerFactory.GetScheduler().Returns(_scheduler); _sut = new SubscriptionUpdatedHandler( _stripeEventService, _stripeEventUtilityService, _organizationService, _stripeFacade, _organizationSponsorshipRenewCommand, _userService, _pushNotificationService, _organizationRepository, _schedulerFactory, _organizationEnableCommand, _organizationDisableCommand, _pricingClient); } [Fact] public async Task HandleAsync_UnpaidOrganizationSubscription_DisablesOrganizationAndSchedulesCancellation() { // Arrange var organizationId = Guid.NewGuid(); var subscriptionId = "sub_123"; var currentPeriodEnd = DateTime.UtcNow.AddDays(30); var subscription = new Subscription { Id = subscriptionId, Status = StripeSubscriptionStatus.Unpaid, CurrentPeriodEnd = currentPeriodEnd, Metadata = new Dictionary { { "organizationId", organizationId.ToString() } }, LatestInvoice = new Invoice { BillingReason = "subscription_cycle" } }; var parsedEvent = new Event { Data = new EventData() }; _stripeEventService.GetSubscription(Arg.Any(), Arg.Any(), Arg.Any>()) .Returns(subscription); _stripeEventUtilityService.GetIdsFromMetadata(Arg.Any>()) .Returns(Tuple.Create(organizationId, null, null)); // Act await _sut.HandleAsync(parsedEvent); // Assert await _organizationDisableCommand.Received(1) .DisableAsync(organizationId, currentPeriodEnd); await _scheduler.Received(1).ScheduleJob( Arg.Is(j => j.Key.Name == $"cancel-sub-{subscriptionId}"), Arg.Is(t => t.Key.Name == $"cancel-trigger-{subscriptionId}")); } [Fact] public async Task HandleAsync_UnpaidUserSubscription_DisablesPremiumAndCancelsSubscription() { // Arrange var userId = Guid.NewGuid(); var subscriptionId = "sub_123"; var currentPeriodEnd = DateTime.UtcNow.AddDays(30); var subscription = new Subscription { Id = subscriptionId, Status = StripeSubscriptionStatus.Unpaid, CurrentPeriodEnd = currentPeriodEnd, Metadata = new Dictionary { { "userId", userId.ToString() } }, Items = new StripeList { Data = new List { new() { Price = new Price { Id = IStripeEventUtilityService.PremiumPlanId } } } } }; var parsedEvent = new Event { Data = new EventData() }; _stripeEventService.GetSubscription(Arg.Any(), Arg.Any(), Arg.Any>()) .Returns(subscription); _stripeEventUtilityService.GetIdsFromMetadata(Arg.Any>()) .Returns(Tuple.Create(null, userId, null)); _stripeFacade.ListInvoices(Arg.Any()) .Returns(new StripeList { Data = new List() }); // Act await _sut.HandleAsync(parsedEvent); // Assert await _userService.Received(1) .DisablePremiumAsync(userId, currentPeriodEnd); await _stripeFacade.Received(1) .CancelSubscription(subscriptionId, Arg.Any()); await _stripeFacade.Received(1) .ListInvoices(Arg.Is(o => o.Status == StripeInvoiceStatus.Open && o.Subscription == subscriptionId)); } [Fact] public async Task HandleAsync_ActiveOrganizationSubscription_EnablesOrganizationAndUpdatesExpiration() { // Arrange var organizationId = Guid.NewGuid(); var currentPeriodEnd = DateTime.UtcNow.AddDays(30); var subscription = new Subscription { Status = StripeSubscriptionStatus.Active, CurrentPeriodEnd = currentPeriodEnd, Metadata = new Dictionary { { "organizationId", organizationId.ToString() } } }; var organization = new Organization { Id = organizationId, PlanType = PlanType.EnterpriseAnnually2023 }; var parsedEvent = new Event { Data = new EventData() }; _stripeEventService.GetSubscription(Arg.Any(), Arg.Any(), Arg.Any>()) .Returns(subscription); _stripeEventUtilityService.GetIdsFromMetadata(Arg.Any>()) .Returns(Tuple.Create(organizationId, null, null)); _organizationRepository.GetByIdAsync(organizationId) .Returns(organization); _stripeFacade.ListInvoices(Arg.Any()) .Returns(new StripeList { Data = new List { new Invoice { Id = "inv_123" } } }); var plan = new Enterprise2023Plan(true); _pricingClient.GetPlanOrThrow(organization.PlanType) .Returns(plan); // Act await _sut.HandleAsync(parsedEvent); // Assert await _organizationEnableCommand.Received(1) .EnableAsync(organizationId); await _organizationService.Received(1) .UpdateExpirationDateAsync(organizationId, currentPeriodEnd); await _pushNotificationService.Received(1) .PushSyncOrganizationStatusAsync(organization); } [Fact] public async Task HandleAsync_ActiveUserSubscription_EnablesPremiumAndUpdatesExpiration() { // Arrange var userId = Guid.NewGuid(); var currentPeriodEnd = DateTime.UtcNow.AddDays(30); var subscription = new Subscription { Status = StripeSubscriptionStatus.Active, CurrentPeriodEnd = currentPeriodEnd, Metadata = new Dictionary { { "userId", userId.ToString() } } }; var parsedEvent = new Event { Data = new EventData() }; _stripeEventService.GetSubscription(Arg.Any(), Arg.Any(), Arg.Any>()) .Returns(subscription); _stripeEventUtilityService.GetIdsFromMetadata(Arg.Any>()) .Returns(Tuple.Create(null, userId, null)); // Act await _sut.HandleAsync(parsedEvent); // Assert await _userService.Received(1) .EnablePremiumAsync(userId, currentPeriodEnd); await _userService.Received(1) .UpdatePremiumExpirationAsync(userId, currentPeriodEnd); } [Fact] public async Task HandleAsync_SponsoredSubscription_RenewsSponsorship() { // Arrange var organizationId = Guid.NewGuid(); var currentPeriodEnd = DateTime.UtcNow.AddDays(30); var subscription = new Subscription { Status = StripeSubscriptionStatus.Active, CurrentPeriodEnd = currentPeriodEnd, Metadata = new Dictionary { { "organizationId", organizationId.ToString() } } }; var parsedEvent = new Event { Data = new EventData() }; _stripeEventService.GetSubscription(Arg.Any(), Arg.Any(), Arg.Any>()) .Returns(subscription); _stripeEventUtilityService.GetIdsFromMetadata(Arg.Any>()) .Returns(Tuple.Create(organizationId, null, null)); _stripeEventUtilityService.IsSponsoredSubscription(subscription) .Returns(true); // Act await _sut.HandleAsync(parsedEvent); // Assert await _organizationSponsorshipRenewCommand.Received(1) .UpdateExpirationDateAsync(organizationId, currentPeriodEnd); } [Fact] public async Task HandleAsync_WhenSubscriptionIsActive_AndOrganizationHasSecretsManagerTrial_AndRemovingSecretsManagerTrial_RemovesPasswordManagerCoupon() { // Arrange var organizationId = Guid.NewGuid(); var subscription = new Subscription { Id = "sub_123", Status = StripeSubscriptionStatus.Active, CurrentPeriodEnd = DateTime.UtcNow.AddDays(10), CustomerId = "cus_123", Items = new StripeList { Data = new List { new() { Plan = new Stripe.Plan { Id = "2023-enterprise-org-seat-annually" } } } }, Customer = new Customer { Balance = 0, Discount = new Discount { Coupon = new Coupon { Id = "sm-standalone" } } }, Discount = new Discount { Coupon = new Coupon { Id = "sm-standalone" } }, Metadata = new Dictionary { { "organizationId", organizationId.ToString() } } }; var organization = new Organization { Id = organizationId, PlanType = PlanType.EnterpriseAnnually2023 }; var plan = new Enterprise2023Plan(true); _pricingClient.GetPlanOrThrow(organization.PlanType) .Returns(plan); var parsedEvent = new Event { Data = new EventData { Object = subscription, PreviousAttributes = JObject.FromObject(new { items = new { data = new[] { new { plan = new { id = "secrets-manager-enterprise-seat-annually" } } } }, Items = new StripeList { Data = new List { new SubscriptionItem { Plan = new Stripe.Plan { Id = "secrets-manager-enterprise-seat-annually" } } } } }) } }; _stripeEventService.GetSubscription(Arg.Any(), Arg.Any(), Arg.Any>()) .Returns(subscription); _stripeEventUtilityService.GetIdsFromMetadata(Arg.Any>()) .Returns(Tuple.Create(organizationId, null, null)); _organizationRepository.GetByIdAsync(organizationId) .Returns(organization); // Act await _sut.HandleAsync(parsedEvent); // Assert await _stripeFacade.Received(1).DeleteCustomerDiscount(subscription.CustomerId); await _stripeFacade.Received(1).DeleteSubscriptionDiscount(subscription.Id); } }