using Bit.Core.Billing.Enums; using Bit.Core.Billing.Services.DiscountAudienceFilters; using Bit.Core.Billing.Services.Implementations; using Bit.Core.Billing.Subscriptions.Entities; using Bit.Core.Billing.Subscriptions.Repositories; using Bit.Core.Entities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using NSubstitute.ReturnsExtensions; using Xunit; namespace Bit.Core.Test.Billing.Services; [SutProviderCustomize] public class SubscriptionDiscountServiceTests { private static IDictionary DiscountDictionary(bool eligibilitySetting) => Enum.GetValues().ToDictionary(t => t, _ => eligibilitySetting); [Theory, BitAutoData] public async Task GetEligibleDiscountsAsync_NoActiveDiscounts_ReturnsEmpty( User user, SutProvider sutProvider) { // Arrange sutProvider.GetDependency() .GetActiveDiscountsAsync() .Returns([]); // Act var result = await sutProvider.Sut.GetEligibleDiscountsAsync(user); // Assert Assert.Empty(result); } [Theory, BitAutoData] public async Task GetEligibleDiscountsAsync_AllUsersDiscount_ReturnsDiscount( User user, SubscriptionDiscount discount, SutProvider sutProvider) { // Arrange discount.AudienceType = DiscountAudienceType.AllUsers; sutProvider.GetDependency() .GetActiveDiscountsAsync() .Returns([discount]); var filter = Substitute.For(); filter.IsUserEligible(user, discount).Returns(DiscountDictionary(true)); sutProvider.GetDependency() .GetFilter(DiscountAudienceType.AllUsers) .Returns(filter); // Act var result = await sutProvider.Sut.GetEligibleDiscountsAsync(user); // Assert Assert.Contains(result, e => e.Discount == discount); } [Theory, BitAutoData] public async Task GetEligibleDiscountsAsync_UserIsEligibleForDiscount_ReturnsDiscount( User user, SubscriptionDiscount discount, SutProvider sutProvider) { // Arrange discount.AudienceType = DiscountAudienceType.UserHasNoPreviousSubscriptions; sutProvider.GetDependency() .GetActiveDiscountsAsync() .Returns([discount]); var filter = Substitute.For(); filter.IsUserEligible(user, discount).Returns(DiscountDictionary(true)); sutProvider.GetDependency() .GetFilter(DiscountAudienceType.UserHasNoPreviousSubscriptions) .Returns(filter); // Act var result = await sutProvider.Sut.GetEligibleDiscountsAsync(user); // Assert Assert.Contains(result, e => e.Discount == discount); } [Theory, BitAutoData] public async Task GetEligibleDiscountsAsync_UserIsIneligibleForDiscount_ReturnsEmpty( User user, SubscriptionDiscount discount, SutProvider sutProvider) { // Arrange discount.AudienceType = DiscountAudienceType.UserHasNoPreviousSubscriptions; sutProvider.GetDependency() .GetActiveDiscountsAsync() .Returns([discount]); var filter = Substitute.For(); filter.IsUserEligible(user, discount).Returns(DiscountDictionary(false)); sutProvider.GetDependency() .GetFilter(DiscountAudienceType.UserHasNoPreviousSubscriptions) .Returns(filter); // Act var result = await sutProvider.Sut.GetEligibleDiscountsAsync(user); // Assert Assert.DoesNotContain(result, e => e.Discount == discount); } [Theory, BitAutoData] public async Task GetEligibleDiscountsAsync_NoFilterForAudienceType_ReturnsEmpty( User user, SubscriptionDiscount discount, SutProvider sutProvider) { // Arrange discount.AudienceType = DiscountAudienceType.UserHasNoPreviousSubscriptions; sutProvider.GetDependency() .GetActiveDiscountsAsync() .Returns([discount]); sutProvider.GetDependency() .GetFilter(DiscountAudienceType.UserHasNoPreviousSubscriptions) .ReturnsNull(); // Act var result = await sutProvider.Sut.GetEligibleDiscountsAsync(user); // Assert Assert.DoesNotContain(result, e => e.Discount == discount); } [Theory, BitAutoData] public async Task GetEligibleDiscountsAsync_MixedDiscounts_ReturnsOnlyEligible( User user, SubscriptionDiscount allUsersDiscount, SubscriptionDiscount eligibleDiscount, SubscriptionDiscount ineligibleDiscount, SutProvider sutProvider) { // Arrange allUsersDiscount.AudienceType = DiscountAudienceType.AllUsers; eligibleDiscount.AudienceType = DiscountAudienceType.UserHasNoPreviousSubscriptions; ineligibleDiscount.AudienceType = DiscountAudienceType.UserHasNoPreviousSubscriptions; sutProvider.GetDependency() .GetActiveDiscountsAsync() .Returns([allUsersDiscount, eligibleDiscount, ineligibleDiscount]); var allUsersFilter = Substitute.For(); allUsersFilter.IsUserEligible(user, allUsersDiscount).Returns(DiscountDictionary(true)); sutProvider.GetDependency() .GetFilter(DiscountAudienceType.AllUsers) .Returns(allUsersFilter); var filter = Substitute.For(); filter.IsUserEligible(user, eligibleDiscount).Returns(DiscountDictionary(true)); filter.IsUserEligible(user, ineligibleDiscount).Returns(DiscountDictionary(false)); sutProvider.GetDependency() .GetFilter(DiscountAudienceType.UserHasNoPreviousSubscriptions) .Returns(filter); // Act var result = await sutProvider.Sut.GetEligibleDiscountsAsync(user); // Assert Assert.Contains(result, e => e.Discount == allUsersDiscount); Assert.Contains(result, e => e.Discount == eligibleDiscount); Assert.DoesNotContain(result, e => e.Discount == ineligibleDiscount); } [Theory, BitAutoData] public async Task ValidateDiscountEligibilityForUserAsync_CouponNotInEligibleDiscounts_ReturnsFalse( User user, SutProvider sutProvider) { // Arrange — no active discounts, so the requested coupon won't be found sutProvider.GetDependency() .GetActiveDiscountsAsync() .Returns([]); // Act var result = await sutProvider.Sut.ValidateDiscountEligibilityForUserAsync(user, ["invalid"], DiscountTierType.Premium); // Assert Assert.False(result); } [Theory, BitAutoData] public async Task ValidateDiscountEligibilityForUserAsync_CouponFound_TierEligible_ReturnsTrue( User user, SubscriptionDiscount discount, SutProvider sutProvider) { // Arrange discount.AudienceType = DiscountAudienceType.AllUsers; discount.StartDate = DateTime.UtcNow.AddDays(-1); discount.EndDate = DateTime.UtcNow.AddDays(30); sutProvider.GetDependency() .GetActiveDiscountsAsync() .Returns([discount]); var filter = Substitute.For(); filter.IsUserEligible(user, discount).Returns(DiscountDictionary(true)); sutProvider.GetDependency() .GetFilter(DiscountAudienceType.AllUsers) .Returns(filter); // Act var result = await sutProvider.Sut.ValidateDiscountEligibilityForUserAsync( user, [discount.StripeCouponId], DiscountTierType.Premium); // Assert Assert.True(result); } [Theory, BitAutoData] public async Task ValidateDiscountEligibilityForUserAsync_CouponFound_TierNotEligible_ReturnsFalse( User user, SubscriptionDiscount discount, SutProvider sutProvider) { // Arrange — discount exists and is active but user is not eligible for this audience type discount.AudienceType = DiscountAudienceType.UserHasNoPreviousSubscriptions; discount.StartDate = DateTime.UtcNow.AddDays(-1); discount.EndDate = DateTime.UtcNow.AddDays(30); sutProvider.GetDependency() .GetActiveDiscountsAsync() .Returns([discount]); var filter = Substitute.For(); filter.IsUserEligible(user, discount).Returns(DiscountDictionary(false)); sutProvider.GetDependency() .GetFilter(DiscountAudienceType.UserHasNoPreviousSubscriptions) .Returns(filter); // Act var result = await sutProvider.Sut.ValidateDiscountEligibilityForUserAsync( user, [discount.StripeCouponId], DiscountTierType.Families); // Assert Assert.False(result); } [Theory, BitAutoData] public async Task ValidateDiscountEligibilityForUserAsync_InactiveDiscount_ReturnsFalse( User user, SubscriptionDiscount discount, SutProvider sutProvider) { // Arrange — expired discount is not returned by GetActiveDiscountsAsync, so won't appear in eligible set discount.StartDate = DateTime.UtcNow.AddDays(-30); discount.EndDate = DateTime.UtcNow.AddDays(-1); sutProvider.GetDependency() .GetActiveDiscountsAsync() .Returns([]); // Act var result = await sutProvider.Sut.ValidateDiscountEligibilityForUserAsync( user, [discount.StripeCouponId], DiscountTierType.Premium); // Assert Assert.False(result); } [Theory, BitAutoData] public async Task ValidateDiscountEligibilityForUserAsync_MultipleCoupons_AllEligible_ReturnsTrue( User user, SubscriptionDiscount discount1, SubscriptionDiscount discount2, SutProvider sutProvider) { // Arrange discount1.AudienceType = DiscountAudienceType.AllUsers; discount1.StartDate = DateTime.UtcNow.AddDays(-1); discount1.EndDate = DateTime.UtcNow.AddDays(30); discount2.AudienceType = DiscountAudienceType.AllUsers; discount2.StartDate = DateTime.UtcNow.AddDays(-1); discount2.EndDate = DateTime.UtcNow.AddDays(30); sutProvider.GetDependency() .GetActiveDiscountsAsync() .Returns([discount1, discount2]); var filter = Substitute.For(); filter.IsUserEligible(user, discount1).Returns(DiscountDictionary(true)); filter.IsUserEligible(user, discount2).Returns(DiscountDictionary(true)); sutProvider.GetDependency() .GetFilter(DiscountAudienceType.AllUsers) .Returns(filter); // Act var result = await sutProvider.Sut.ValidateDiscountEligibilityForUserAsync( user, [discount1.StripeCouponId, discount2.StripeCouponId], DiscountTierType.Premium); // Assert Assert.True(result); } [Theory, BitAutoData] public async Task ValidateDiscountEligibilityForUserAsync_MultipleCoupons_OneNotEligible_ReturnsFalse( User user, SubscriptionDiscount discount1, SubscriptionDiscount discount2, SutProvider sutProvider) { // Arrange — discount1 is eligible, discount2 is not discount1.AudienceType = DiscountAudienceType.AllUsers; discount1.StartDate = DateTime.UtcNow.AddDays(-1); discount1.EndDate = DateTime.UtcNow.AddDays(30); discount2.AudienceType = DiscountAudienceType.UserHasNoPreviousSubscriptions; discount2.StartDate = DateTime.UtcNow.AddDays(-1); discount2.EndDate = DateTime.UtcNow.AddDays(30); sutProvider.GetDependency() .GetActiveDiscountsAsync() .Returns([discount1, discount2]); var allUsersFilter = Substitute.For(); allUsersFilter.IsUserEligible(user, discount1).Returns(DiscountDictionary(true)); sutProvider.GetDependency() .GetFilter(DiscountAudienceType.AllUsers) .Returns(allUsersFilter); var restrictedFilter = Substitute.For(); restrictedFilter.IsUserEligible(user, discount2).Returns(DiscountDictionary(false)); sutProvider.GetDependency() .GetFilter(DiscountAudienceType.UserHasNoPreviousSubscriptions) .Returns(restrictedFilter); // Act var result = await sutProvider.Sut.ValidateDiscountEligibilityForUserAsync( user, [discount1.StripeCouponId, discount2.StripeCouponId], DiscountTierType.Premium); // Assert Assert.False(result); } }