using Bit.Api.Models.Response; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Models.Business; using Bit.Core.Entities; using Bit.Core.Models.Business; using Bit.Test.Common.AutoFixture.Attributes; using Stripe; using Xunit; namespace Bit.Api.Test.Models.Response; public class SubscriptionResponseModelTests { [Theory] [BitAutoData] public void Constructor_IncludeMilestone2DiscountTrueMatchingCouponId_ReturnsDiscount( User user, UserLicense license) { // Arrange var subscriptionInfo = new SubscriptionInfo { CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount { Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, // Matching coupon ID Active = true, PercentOff = 20m, AmountOff = null, AppliesTo = new List { "product1" } } }; // Act var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: true); // Assert Assert.NotNull(result.CustomerDiscount); Assert.Equal(StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, result.CustomerDiscount.Id); Assert.True(result.CustomerDiscount.Active); Assert.Equal(20m, result.CustomerDiscount.PercentOff); Assert.Null(result.CustomerDiscount.AmountOff); Assert.NotNull(result.CustomerDiscount.AppliesTo); Assert.Single(result.CustomerDiscount.AppliesTo); } [Theory] [BitAutoData] public void Constructor_IncludeMilestone2DiscountTrueNonMatchingCouponId_ReturnsNull( User user, UserLicense license) { // Arrange var subscriptionInfo = new SubscriptionInfo { CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount { Id = "different-coupon-id", // Non-matching coupon ID Active = true, PercentOff = 20m, AmountOff = null, AppliesTo = new List { "product1" } } }; // Act var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: true); // Assert Assert.Null(result.CustomerDiscount); } [Theory] [BitAutoData] public void Constructor_IncludeMilestone2DiscountFalseMatchingCouponId_ReturnsNull( User user, UserLicense license) { // Arrange var subscriptionInfo = new SubscriptionInfo { CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount { Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, // Matching coupon ID Active = true, PercentOff = 20m, AmountOff = null, AppliesTo = new List { "product1" } } }; // Act var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: false); // Assert - Should be null because includeMilestone2Discount is false Assert.Null(result.CustomerDiscount); } [Theory] [BitAutoData] public void Constructor_NullCustomerDiscount_ReturnsNull( User user, UserLicense license) { // Arrange var subscriptionInfo = new SubscriptionInfo { CustomerDiscount = null }; // Act var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: true); // Assert Assert.Null(result.CustomerDiscount); } [Theory] [BitAutoData] public void Constructor_AmountOffDiscountMatchingCouponId_ReturnsDiscount( User user, UserLicense license) { // Arrange var subscriptionInfo = new SubscriptionInfo { CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount { Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, Active = true, PercentOff = null, AmountOff = 14.00m, // Already converted from cents in BillingCustomerDiscount AppliesTo = new List() } }; // Act var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: true); // Assert Assert.NotNull(result.CustomerDiscount); Assert.Equal(StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, result.CustomerDiscount.Id); Assert.Null(result.CustomerDiscount.PercentOff); Assert.Equal(14.00m, result.CustomerDiscount.AmountOff); } [Theory] [BitAutoData] public void Constructor_DefaultIncludeMilestone2DiscountParameter_ReturnsNull( User user, UserLicense license) { // Arrange var subscriptionInfo = new SubscriptionInfo { CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount { Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, Active = true, PercentOff = 20m } }; // Act - Using default parameter (includeMilestone2Discount defaults to false) var result = new SubscriptionResponseModel(user, subscriptionInfo, license); // Assert Assert.Null(result.CustomerDiscount); } [Theory] [BitAutoData] public void Constructor_NullDiscountIdIncludeMilestone2DiscountTrue_ReturnsNull( User user, UserLicense license) { // Arrange var subscriptionInfo = new SubscriptionInfo { CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount { Id = null, // Null discount ID Active = true, PercentOff = 20m, AmountOff = null, AppliesTo = new List { "product1" } } }; // Act var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: true); // Assert Assert.Null(result.CustomerDiscount); } [Theory] [BitAutoData] public void Constructor_MatchingCouponIdInactiveDiscount_ReturnsNull( User user, UserLicense license) { // Arrange var subscriptionInfo = new SubscriptionInfo { CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount { Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, // Matching coupon ID Active = false, // Inactive discount PercentOff = 20m, AmountOff = null, AppliesTo = new List { "product1" } } }; // Act var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: true); // Assert Assert.Null(result.CustomerDiscount); } [Theory] [BitAutoData] public void Constructor_UserOnly_SetsBasicProperties(User user) { // Arrange user.Storage = 5368709120; // 5 GB in bytes user.MaxStorageGb = (short)10; user.PremiumExpirationDate = DateTime.UtcNow.AddMonths(12); // Act var result = new SubscriptionResponseModel(user); // Assert Assert.NotNull(result.StorageName); Assert.Equal(5.0, result.StorageGb); Assert.Equal((short)10, result.MaxStorageGb); Assert.Equal(user.PremiumExpirationDate, result.Expiration); Assert.Null(result.License); Assert.Null(result.CustomerDiscount); } [Theory] [BitAutoData] public void Constructor_UserAndLicense_IncludesLicense(User user, UserLicense license) { // Arrange user.Storage = 1073741824; // 1 GB in bytes user.MaxStorageGb = (short)5; // Act var result = new SubscriptionResponseModel(user, license); // Assert Assert.NotNull(result.License); Assert.Equal(license, result.License); Assert.Equal(1.0, result.StorageGb); Assert.Null(result.CustomerDiscount); } [Theory] [BitAutoData] public void Constructor_NullStorage_SetsStorageToZero(User user) { // Arrange user.Storage = null; // Act var result = new SubscriptionResponseModel(user); // Assert Assert.Null(result.StorageName); Assert.Equal(0, result.StorageGb); Assert.Null(result.CustomerDiscount); } [Theory] [BitAutoData] public void Constructor_NullLicense_ExcludesLicense(User user) { // Act var result = new SubscriptionResponseModel(user, null); // Assert Assert.Null(result.License); Assert.Null(result.CustomerDiscount); } [Theory] [BitAutoData] public void Constructor_BothPercentOffAndAmountOffPresent_HandlesEdgeCase( User user, UserLicense license) { // Arrange - Edge case: Both PercentOff and AmountOff present // This tests the scenario where Stripe coupon has both discount types var subscriptionInfo = new SubscriptionInfo { CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount { Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, Active = true, PercentOff = 25m, AmountOff = 20.00m, // Already converted from cents AppliesTo = new List { "prod_premium" } } }; // Act var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: true); // Assert - Both values should be preserved Assert.NotNull(result.CustomerDiscount); Assert.Equal(StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, result.CustomerDiscount.Id); Assert.Equal(25m, result.CustomerDiscount.PercentOff); Assert.Equal(20.00m, result.CustomerDiscount.AmountOff); Assert.NotNull(result.CustomerDiscount.AppliesTo); Assert.Single(result.CustomerDiscount.AppliesTo); } [Theory] [BitAutoData] public void Constructor_WithSubscriptionAndInvoice_MapsAllProperties( User user, UserLicense license) { // Arrange - Test with Subscription, UpcomingInvoice, and CustomerDiscount var stripeSubscription = new Subscription { Id = "sub_test123", Status = "active", CollectionMethod = "charge_automatically" }; var stripeInvoice = new Invoice { AmountDue = 1500, // 1500 cents = $15.00 Created = DateTime.UtcNow.AddDays(7) }; var subscriptionInfo = new SubscriptionInfo { Subscription = new SubscriptionInfo.BillingSubscription(stripeSubscription), UpcomingInvoice = new SubscriptionInfo.BillingUpcomingInvoice(stripeInvoice), CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount { Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, Active = true, PercentOff = 20m, AmountOff = null, AppliesTo = new List { "prod_premium" } } }; // Act var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: true); // Assert - Verify all properties are mapped correctly Assert.NotNull(result.Subscription); Assert.Equal("active", result.Subscription.Status); Assert.Equal(14, result.Subscription.GracePeriod); // charge_automatically = 14 days Assert.NotNull(result.UpcomingInvoice); Assert.Equal(15.00m, result.UpcomingInvoice.Amount); Assert.NotNull(result.UpcomingInvoice.Date); Assert.NotNull(result.CustomerDiscount); Assert.Equal(StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, result.CustomerDiscount.Id); Assert.True(result.CustomerDiscount.Active); Assert.Equal(20m, result.CustomerDiscount.PercentOff); } [Theory] [BitAutoData] public void Constructor_WithNullSubscriptionAndInvoice_HandlesNullsGracefully( User user, UserLicense license) { // Arrange - Test with null Subscription and UpcomingInvoice var subscriptionInfo = new SubscriptionInfo { Subscription = null, UpcomingInvoice = null, CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount { Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, Active = true, PercentOff = 20m } }; // Act var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: true); // Assert - Null Subscription and UpcomingInvoice should be handled gracefully Assert.Null(result.Subscription); Assert.Null(result.UpcomingInvoice); Assert.NotNull(result.CustomerDiscount); } }