using Bit.Billing.Services; using Bit.Billing.Services.Implementations; using Bit.Billing.Test.Utilities; using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Repositories; using Bit.Core.Enums; using Bit.Core.Utilities; using FluentAssertions; using Microsoft.Extensions.Logging; using NSubstitute; using Stripe; using Xunit; namespace Bit.Billing.Test.Services; public class ProviderEventServiceTests { private readonly IProviderInvoiceItemRepository _providerInvoiceItemRepository = Substitute.For(); private readonly IProviderOrganizationRepository _providerOrganizationRepository = Substitute.For(); private readonly IProviderPlanRepository _providerPlanRepository = Substitute.For(); private readonly IStripeEventService _stripeEventService = Substitute.For(); private readonly IStripeFacade _stripeFacade = Substitute.For(); private readonly ProviderEventService _providerEventService; public ProviderEventServiceTests() { _providerEventService = new ProviderEventService( Substitute.For>(), _providerInvoiceItemRepository, _providerOrganizationRepository, _providerPlanRepository, _stripeEventService, _stripeFacade); } #region TryRecordInvoiceLineItems [Fact] public async Task TryRecordInvoiceLineItems_EventTypeNotInvoiceCreatedOrInvoiceFinalized_NoOp() { // Arrange var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.PaymentMethodAttached); // Act await _providerEventService.TryRecordInvoiceLineItems(stripeEvent); // Assert await _stripeEventService.DidNotReceiveWithAnyArgs().GetInvoice(Arg.Any()); } [Fact] public async Task TryRecordInvoiceLineItems_EventNotProviderRelated_NoOp() { // Arrange var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceCreated); const string subscriptionId = "sub_1"; var invoice = new Invoice { SubscriptionId = subscriptionId }; _stripeEventService.GetInvoice(stripeEvent).Returns(invoice); var subscription = new Subscription { Metadata = new Dictionary { { "organizationId", Guid.NewGuid().ToString() } } }; _stripeFacade.GetSubscription(subscriptionId).Returns(subscription); // Act await _providerEventService.TryRecordInvoiceLineItems(stripeEvent); // Assert await _providerOrganizationRepository.DidNotReceiveWithAnyArgs().GetManyDetailsByProviderAsync(Arg.Any()); } [Fact] public async Task TryRecordInvoiceLineItems_InvoiceCreated_MisconfiguredProviderPlans_ThrowsException() { // Arrange var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceCreated); const string subscriptionId = "sub_1"; var providerId = Guid.NewGuid(); var invoice = new Invoice { SubscriptionId = subscriptionId }; _stripeEventService.GetInvoice(stripeEvent).Returns(invoice); var subscription = new Subscription { Metadata = new Dictionary { { "providerId", providerId.ToString() } } }; _stripeFacade.GetSubscription(subscriptionId).Returns(subscription); var providerPlans = new List { new () { Id = Guid.NewGuid(), ProviderId = providerId, PlanType = PlanType.TeamsMonthly, AllocatedSeats = 0, PurchasedSeats = 0, SeatMinimum = 100 } }; _providerPlanRepository.GetByProviderId(providerId).Returns(providerPlans); // Act var function = async () => await _providerEventService.TryRecordInvoiceLineItems(stripeEvent); // Assert await function .Should() .ThrowAsync() .WithMessage("Cannot record invoice line items for Provider with missing or misconfigured provider plans"); } [Fact] public async Task TryRecordInvoiceLineItems_InvoiceCreated_Succeeds() { // Arrange var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceCreated); const string subscriptionId = "sub_1"; var providerId = Guid.NewGuid(); var invoice = new Invoice { Id = "invoice_1", Number = "A", SubscriptionId = subscriptionId, Discount = new Discount { Coupon = new Coupon { PercentOff = 35 } } }; _stripeEventService.GetInvoice(stripeEvent).Returns(invoice); var subscription = new Subscription { Metadata = new Dictionary { { "providerId", providerId.ToString() } } }; _stripeFacade.GetSubscription(subscriptionId).Returns(subscription); var clients = new List { new () { OrganizationName = "Client 1", Plan = "Teams (Monthly)", Seats = 50, UserCount = 30, Status = OrganizationStatusType.Managed }, new () { OrganizationName = "Client 2", Plan = "Enterprise (Monthly)", Seats = 50, UserCount = 30, Status = OrganizationStatusType.Managed } }; _providerOrganizationRepository.GetManyDetailsByProviderAsync(providerId).Returns(clients); var providerPlans = new List { new () { Id = Guid.NewGuid(), ProviderId = providerId, PlanType = PlanType.TeamsMonthly, AllocatedSeats = 50, PurchasedSeats = 0, SeatMinimum = 100 }, new () { Id = Guid.NewGuid(), ProviderId = providerId, PlanType = PlanType.EnterpriseMonthly, AllocatedSeats = 50, PurchasedSeats = 0, SeatMinimum = 100 } }; _providerPlanRepository.GetByProviderId(providerId).Returns(providerPlans); // Act await _providerEventService.TryRecordInvoiceLineItems(stripeEvent); // Assert var teamsPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); var enterprisePlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly); await _providerInvoiceItemRepository.Received(1).CreateAsync(Arg.Is( options => options.ProviderId == providerId && options.InvoiceId == invoice.Id && options.InvoiceNumber == invoice.Number && options.ClientName == "Client 1" && options.PlanName == "Teams (Monthly)" && options.AssignedSeats == 50 && options.UsedSeats == 30 && options.Total == options.AssignedSeats * teamsPlan.PasswordManager.ProviderPortalSeatPrice * 0.65M)); await _providerInvoiceItemRepository.Received(1).CreateAsync(Arg.Is( options => options.ProviderId == providerId && options.InvoiceId == invoice.Id && options.InvoiceNumber == invoice.Number && options.ClientName == "Client 2" && options.PlanName == "Enterprise (Monthly)" && options.AssignedSeats == 50 && options.UsedSeats == 30 && options.Total == options.AssignedSeats * enterprisePlan.PasswordManager.ProviderPortalSeatPrice * 0.65M)); await _providerInvoiceItemRepository.Received(1).CreateAsync(Arg.Is( options => options.ProviderId == providerId && options.InvoiceId == invoice.Id && options.InvoiceNumber == invoice.Number && options.ClientName == "Unassigned seats" && options.PlanName == "Teams (Monthly)" && options.AssignedSeats == 50 && options.UsedSeats == 0 && options.Total == options.AssignedSeats * teamsPlan.PasswordManager.ProviderPortalSeatPrice * 0.65M)); await _providerInvoiceItemRepository.Received(1).CreateAsync(Arg.Is( options => options.ProviderId == providerId && options.InvoiceId == invoice.Id && options.InvoiceNumber == invoice.Number && options.ClientName == "Unassigned seats" && options.PlanName == "Enterprise (Monthly)" && options.AssignedSeats == 50 && options.UsedSeats == 0 && options.Total == options.AssignedSeats * enterprisePlan.PasswordManager.ProviderPortalSeatPrice * 0.65M)); } [Fact] public async Task TryRecordInvoiceLineItems_InvoiceFinalized_Succeeds() { // Arrange var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceFinalized); const string subscriptionId = "sub_1"; var providerId = Guid.NewGuid(); var invoice = new Invoice { Id = "invoice_1", Number = "A", SubscriptionId = subscriptionId }; _stripeEventService.GetInvoice(stripeEvent).Returns(invoice); var subscription = new Subscription { Metadata = new Dictionary { { "providerId", providerId.ToString() } } }; _stripeFacade.GetSubscription(subscriptionId).Returns(subscription); var invoiceItems = new List { new () { Id = Guid.NewGuid(), ClientName = "Client 1" }, new () { Id = Guid.NewGuid(), ClientName = "Client 2" } }; _providerInvoiceItemRepository.GetByInvoiceId(invoice.Id).Returns(invoiceItems); // Act await _providerEventService.TryRecordInvoiceLineItems(stripeEvent); // Assert await _providerInvoiceItemRepository.Received(2).ReplaceAsync(Arg.Is( options => options.InvoiceNumber == "A")); } #endregion }