using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Tax.Requests; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Services; using Bit.Core.Test.Billing.Mocks.Plans; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using Stripe; using Xunit; namespace Bit.Core.Test.Services; [SutProviderCustomize] public class StripePaymentServiceTests { [Theory] [BitAutoData] public async Task PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesWithoutAdditionalStorage( SutProvider sutProvider) { var familiesPlan = new FamiliesPlan(); sutProvider.GetDependency() .GetPlanOrThrow(Arg.Is(p => p == PlanType.FamiliesAnnually)) .Returns(familiesPlan); var parameters = new PreviewOrganizationInvoiceRequestBody { PasswordManager = new OrganizationPasswordManagerRequestModel { Plan = PlanType.FamiliesAnnually, AdditionalStorage = 0 }, TaxInformation = new TaxInformationRequestModel { Country = "FR", PostalCode = "12345" } }; sutProvider.GetDependency() .InvoiceCreatePreviewAsync(Arg.Is(p => p.Currency == "usd" && p.SubscriptionDetails.Items.Any(x => x.Plan == familiesPlan.PasswordManager.StripePlanId && x.Quantity == 1) && p.SubscriptionDetails.Items.Any(x => x.Plan == familiesPlan.PasswordManager.StripeStoragePlanId && x.Quantity == 0))) .Returns(new Invoice { TotalExcludingTax = 4000, TotalTaxes = [new InvoiceTotalTax { Amount = 800 }], Total = 4800 }); var actual = await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null); Assert.Equal(8M, actual.TaxAmount); Assert.Equal(48M, actual.TotalAmount); Assert.Equal(40M, actual.TaxableBaseAmount); } [Theory] [BitAutoData] public async Task PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesWithAdditionalStorage( SutProvider sutProvider) { var familiesPlan = new FamiliesPlan(); sutProvider.GetDependency() .GetPlanOrThrow(Arg.Is(p => p == PlanType.FamiliesAnnually)) .Returns(familiesPlan); var parameters = new PreviewOrganizationInvoiceRequestBody { PasswordManager = new OrganizationPasswordManagerRequestModel { Plan = PlanType.FamiliesAnnually, AdditionalStorage = 1 }, TaxInformation = new TaxInformationRequestModel { Country = "FR", PostalCode = "12345" } }; sutProvider.GetDependency() .InvoiceCreatePreviewAsync(Arg.Is(p => p.Currency == "usd" && p.SubscriptionDetails.Items.Any(x => x.Plan == familiesPlan.PasswordManager.StripePlanId && x.Quantity == 1) && p.SubscriptionDetails.Items.Any(x => x.Plan == familiesPlan.PasswordManager.StripeStoragePlanId && x.Quantity == 1))) .Returns(new Invoice { TotalExcludingTax = 4000, TotalTaxes = [new InvoiceTotalTax { Amount = 800 }], Total = 4800 }); var actual = await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null); Assert.Equal(8M, actual.TaxAmount); Assert.Equal(48M, actual.TotalAmount); Assert.Equal(40M, actual.TaxableBaseAmount); } [Theory] [BitAutoData] public async Task PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesForEnterpriseWithoutAdditionalStorage( SutProvider sutProvider) { var familiesPlan = new FamiliesPlan(); sutProvider.GetDependency() .GetPlanOrThrow(Arg.Is(p => p == PlanType.FamiliesAnnually)) .Returns(familiesPlan); var parameters = new PreviewOrganizationInvoiceRequestBody { PasswordManager = new OrganizationPasswordManagerRequestModel { Plan = PlanType.FamiliesAnnually, SponsoredPlan = PlanSponsorshipType.FamiliesForEnterprise, AdditionalStorage = 0 }, TaxInformation = new TaxInformationRequestModel { Country = "FR", PostalCode = "12345" } }; sutProvider.GetDependency() .InvoiceCreatePreviewAsync(Arg.Is(p => p.Currency == "usd" && p.SubscriptionDetails.Items.Any(x => x.Plan == "2021-family-for-enterprise-annually" && x.Quantity == 1) && p.SubscriptionDetails.Items.Any(x => x.Plan == familiesPlan.PasswordManager.StripeStoragePlanId && x.Quantity == 0))) .Returns(new Invoice { TotalExcludingTax = 0, TotalTaxes = [new InvoiceTotalTax { Amount = 0 }], Total = 0 }); var actual = await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null); Assert.Equal(0M, actual.TaxAmount); Assert.Equal(0M, actual.TotalAmount); Assert.Equal(0M, actual.TaxableBaseAmount); } [Theory] [BitAutoData] public async Task PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesForEnterpriseWithAdditionalStorage( SutProvider sutProvider) { var familiesPlan = new FamiliesPlan(); sutProvider.GetDependency() .GetPlanOrThrow(Arg.Is(p => p == PlanType.FamiliesAnnually)) .Returns(familiesPlan); var parameters = new PreviewOrganizationInvoiceRequestBody { PasswordManager = new OrganizationPasswordManagerRequestModel { Plan = PlanType.FamiliesAnnually, SponsoredPlan = PlanSponsorshipType.FamiliesForEnterprise, AdditionalStorage = 1 }, TaxInformation = new TaxInformationRequestModel { Country = "FR", PostalCode = "12345" } }; sutProvider.GetDependency() .InvoiceCreatePreviewAsync(Arg.Is(p => p.Currency == "usd" && p.SubscriptionDetails.Items.Any(x => x.Plan == "2021-family-for-enterprise-annually" && x.Quantity == 1) && p.SubscriptionDetails.Items.Any(x => x.Plan == familiesPlan.PasswordManager.StripeStoragePlanId && x.Quantity == 1))) .Returns(new Invoice { TotalExcludingTax = 400, TotalTaxes = [new InvoiceTotalTax { Amount = 8 }], Total = 408 }); var actual = await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null); Assert.Equal(0.08M, actual.TaxAmount); Assert.Equal(4.08M, actual.TotalAmount); Assert.Equal(4M, actual.TaxableBaseAmount); } [Theory] [BitAutoData] public async Task PreviewInvoiceAsync_USBased_PersonalUse_SetsAutomaticTaxEnabled(SutProvider sutProvider) { // Arrange var familiesPlan = new FamiliesPlan(); sutProvider.GetDependency() .GetPlanOrThrow(Arg.Is(p => p == PlanType.FamiliesAnnually)) .Returns(familiesPlan); var parameters = new PreviewOrganizationInvoiceRequestBody { PasswordManager = new OrganizationPasswordManagerRequestModel { Plan = PlanType.FamiliesAnnually }, TaxInformation = new TaxInformationRequestModel { Country = "US", PostalCode = "12345" } }; var stripeAdapter = sutProvider.GetDependency(); stripeAdapter .InvoiceCreatePreviewAsync(Arg.Any()) .Returns(new Invoice { TotalExcludingTax = 400, TotalTaxes = [new InvoiceTotalTax { Amount = 8 }], Total = 408 }); // Act await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null); // Assert await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => options.AutomaticTax.Enabled == true )); } [Theory] [BitAutoData] public async Task PreviewInvoiceAsync_USBased_BusinessUse_SetsAutomaticTaxEnabled(SutProvider sutProvider) { // Arrange var plan = new EnterprisePlan(true); sutProvider.GetDependency() .GetPlanOrThrow(Arg.Is(p => p == PlanType.EnterpriseAnnually)) .Returns(plan); var parameters = new PreviewOrganizationInvoiceRequestBody { PasswordManager = new OrganizationPasswordManagerRequestModel { Plan = PlanType.EnterpriseAnnually }, TaxInformation = new TaxInformationRequestModel { Country = "US", PostalCode = "12345" } }; var stripeAdapter = sutProvider.GetDependency(); stripeAdapter .InvoiceCreatePreviewAsync(Arg.Any()) .Returns(new Invoice { TotalExcludingTax = 400, TotalTaxes = [new InvoiceTotalTax { Amount = 8 }], Total = 408 }); // Act await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null); // Assert await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => options.AutomaticTax.Enabled == true )); } [Theory] [BitAutoData] public async Task PreviewInvoiceAsync_NonUSBased_PersonalUse_SetsAutomaticTaxEnabled(SutProvider sutProvider) { // Arrange var familiesPlan = new FamiliesPlan(); sutProvider.GetDependency() .GetPlanOrThrow(Arg.Is(p => p == PlanType.FamiliesAnnually)) .Returns(familiesPlan); var parameters = new PreviewOrganizationInvoiceRequestBody { PasswordManager = new OrganizationPasswordManagerRequestModel { Plan = PlanType.FamiliesAnnually }, TaxInformation = new TaxInformationRequestModel { Country = "FR", PostalCode = "12345" } }; var stripeAdapter = sutProvider.GetDependency(); stripeAdapter .InvoiceCreatePreviewAsync(Arg.Any()) .Returns(new Invoice { TotalExcludingTax = 400, TotalTaxes = [new InvoiceTotalTax { Amount = 8 }], Total = 408 }); // Act await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null); // Assert await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => options.AutomaticTax.Enabled == true )); } [Theory] [BitAutoData] public async Task PreviewInvoiceAsync_NonUSBased_BusinessUse_SetsAutomaticTaxEnabled(SutProvider sutProvider) { // Arrange var plan = new EnterprisePlan(true); sutProvider.GetDependency() .GetPlanOrThrow(Arg.Is(p => p == PlanType.EnterpriseAnnually)) .Returns(plan); var parameters = new PreviewOrganizationInvoiceRequestBody { PasswordManager = new OrganizationPasswordManagerRequestModel { Plan = PlanType.EnterpriseAnnually }, TaxInformation = new TaxInformationRequestModel { Country = "FR", PostalCode = "12345" } }; var stripeAdapter = sutProvider.GetDependency(); stripeAdapter .InvoiceCreatePreviewAsync(Arg.Any()) .Returns(new Invoice { TotalExcludingTax = 400, TotalTaxes = [new InvoiceTotalTax { Amount = 8 }], Total = 408 }); // Act await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null); // Assert await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => options.AutomaticTax.Enabled == true )); } [Theory] [BitAutoData] public async Task PreviewInvoiceAsync_USBased_PersonalUse_DoesNotSetTaxExempt(SutProvider sutProvider) { // Arrange var familiesPlan = new FamiliesPlan(); sutProvider.GetDependency() .GetPlanOrThrow(Arg.Is(p => p == PlanType.FamiliesAnnually)) .Returns(familiesPlan); var parameters = new PreviewOrganizationInvoiceRequestBody { PasswordManager = new OrganizationPasswordManagerRequestModel { Plan = PlanType.FamiliesAnnually }, TaxInformation = new TaxInformationRequestModel { Country = "US", PostalCode = "12345" } }; var stripeAdapter = sutProvider.GetDependency(); stripeAdapter .InvoiceCreatePreviewAsync(Arg.Any()) .Returns(new Invoice { TotalExcludingTax = 400, TotalTaxes = [new InvoiceTotalTax { Amount = 8 }], Total = 408 }); // Act await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null); // Assert await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => options.CustomerDetails.TaxExempt == null )); } [Theory] [BitAutoData] public async Task PreviewInvoiceAsync_USBased_BusinessUse_DoesNotSetTaxExempt(SutProvider sutProvider) { // Arrange var plan = new EnterprisePlan(true); sutProvider.GetDependency() .GetPlanOrThrow(Arg.Is(p => p == PlanType.EnterpriseAnnually)) .Returns(plan); var parameters = new PreviewOrganizationInvoiceRequestBody { PasswordManager = new OrganizationPasswordManagerRequestModel { Plan = PlanType.EnterpriseAnnually }, TaxInformation = new TaxInformationRequestModel { Country = "US", PostalCode = "12345" } }; var stripeAdapter = sutProvider.GetDependency(); stripeAdapter .InvoiceCreatePreviewAsync(Arg.Any()) .Returns(new Invoice { TotalExcludingTax = 400, TotalTaxes = [new InvoiceTotalTax { Amount = 8 }], Total = 408 }); // Act await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null); // Assert await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => options.CustomerDetails.TaxExempt == null )); } [Theory] [BitAutoData] public async Task PreviewInvoiceAsync_NonUSBased_PersonalUse_DoesNotSetTaxExempt(SutProvider sutProvider) { // Arrange var familiesPlan = new FamiliesPlan(); sutProvider.GetDependency() .GetPlanOrThrow(Arg.Is(p => p == PlanType.FamiliesAnnually)) .Returns(familiesPlan); var parameters = new PreviewOrganizationInvoiceRequestBody { PasswordManager = new OrganizationPasswordManagerRequestModel { Plan = PlanType.FamiliesAnnually }, TaxInformation = new TaxInformationRequestModel { Country = "FR", PostalCode = "12345" } }; var stripeAdapter = sutProvider.GetDependency(); stripeAdapter .InvoiceCreatePreviewAsync(Arg.Any()) .Returns(new Invoice { TotalExcludingTax = 400, TotalTaxes = [new InvoiceTotalTax { Amount = 8 }], Total = 408 }); // Act await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null); // Assert await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => options.CustomerDetails.TaxExempt == null )); } [Theory] [BitAutoData] public async Task PreviewInvoiceAsync_NonUSBased_BusinessUse_SetsTaxExemptReverse(SutProvider sutProvider) { // Arrange var plan = new EnterprisePlan(true); sutProvider.GetDependency() .GetPlanOrThrow(Arg.Is(p => p == PlanType.EnterpriseAnnually)) .Returns(plan); var parameters = new PreviewOrganizationInvoiceRequestBody { PasswordManager = new OrganizationPasswordManagerRequestModel { Plan = PlanType.EnterpriseAnnually }, TaxInformation = new TaxInformationRequestModel { Country = "FR", PostalCode = "12345" } }; var stripeAdapter = sutProvider.GetDependency(); stripeAdapter .InvoiceCreatePreviewAsync(Arg.Any()) .Returns(new Invoice { TotalExcludingTax = 400, TotalTaxes = [new InvoiceTotalTax { Amount = 8 }], Total = 408 }); // Act await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null); // Assert await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => options.CustomerDetails.TaxExempt == StripeConstants.TaxExempt.Reverse )); } [Theory] [BitAutoData] public async Task GetSubscriptionAsync_WithCustomerDiscount_ReturnsDiscountFromCustomer( SutProvider sutProvider, User subscriber) { // Arrange subscriber.Gateway = GatewayType.Stripe; subscriber.GatewayCustomerId = "cus_test123"; subscriber.GatewaySubscriptionId = "sub_test123"; var customerDiscount = new Discount { Coupon = new Coupon { Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, PercentOff = 20m, AmountOff = 1400 }, End = null }; var subscription = new Subscription { Id = "sub_test123", Status = "active", CollectionMethod = "charge_automatically", Customer = new Customer { Discount = customerDiscount }, Discounts = new List(), // Empty list Items = new StripeList { Data = [] } }; sutProvider.GetDependency() .SubscriptionGetAsync( subscriber.GatewaySubscriptionId, Arg.Any()) .Returns(subscription); // Act var result = await sutProvider.Sut.GetSubscriptionAsync(subscriber); // Assert Assert.NotNull(result.CustomerDiscount); Assert.Equal(StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, result.CustomerDiscount.Id); Assert.Equal(20m, result.CustomerDiscount.PercentOff); Assert.Equal(14.00m, result.CustomerDiscount.AmountOff); // Converted from cents } [Theory] [BitAutoData] public async Task GetSubscriptionAsync_WithoutCustomerDiscount_FallsBackToSubscriptionDiscounts( SutProvider sutProvider, User subscriber) { // Arrange subscriber.Gateway = GatewayType.Stripe; subscriber.GatewayCustomerId = "cus_test123"; subscriber.GatewaySubscriptionId = "sub_test123"; var subscriptionDiscount = new Discount { Coupon = new Coupon { Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, PercentOff = 15m, AmountOff = null }, End = null }; var subscription = new Subscription { Id = "sub_test123", Status = "active", CollectionMethod = "charge_automatically", Customer = new Customer { Discount = null // No customer discount }, Discounts = new List { subscriptionDiscount }, Items = new StripeList { Data = [] } }; sutProvider.GetDependency() .SubscriptionGetAsync( subscriber.GatewaySubscriptionId, Arg.Any()) .Returns(subscription); // Act var result = await sutProvider.Sut.GetSubscriptionAsync(subscriber); // Assert - Should use subscription discount as fallback Assert.NotNull(result.CustomerDiscount); Assert.Equal(StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, result.CustomerDiscount.Id); Assert.Equal(15m, result.CustomerDiscount.PercentOff); } [Theory] [BitAutoData] public async Task GetSubscriptionAsync_WithBothDiscounts_PrefersCustomerDiscount( SutProvider sutProvider, User subscriber) { // Arrange subscriber.Gateway = GatewayType.Stripe; subscriber.GatewayCustomerId = "cus_test123"; subscriber.GatewaySubscriptionId = "sub_test123"; var customerDiscount = new Discount { Coupon = new Coupon { Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, PercentOff = 25m }, End = null }; var subscriptionDiscount = new Discount { Coupon = new Coupon { Id = "different-coupon-id", PercentOff = 10m }, End = null }; var subscription = new Subscription { Id = "sub_test123", Status = "active", CollectionMethod = "charge_automatically", Customer = new Customer { Discount = customerDiscount // Should prefer this }, Discounts = new List { subscriptionDiscount }, Items = new StripeList { Data = [] } }; sutProvider.GetDependency() .SubscriptionGetAsync( subscriber.GatewaySubscriptionId, Arg.Any()) .Returns(subscription); // Act var result = await sutProvider.Sut.GetSubscriptionAsync(subscriber); // Assert - Should prefer customer discount over subscription discount Assert.NotNull(result.CustomerDiscount); Assert.Equal(StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, result.CustomerDiscount.Id); Assert.Equal(25m, result.CustomerDiscount.PercentOff); } [Theory] [BitAutoData] public async Task GetSubscriptionAsync_WithNoDiscounts_ReturnsNullDiscount( SutProvider sutProvider, User subscriber) { // Arrange subscriber.Gateway = GatewayType.Stripe; subscriber.GatewayCustomerId = "cus_test123"; subscriber.GatewaySubscriptionId = "sub_test123"; var subscription = new Subscription { Id = "sub_test123", Status = "active", CollectionMethod = "charge_automatically", Customer = new Customer { Discount = null }, Discounts = new List(), // Empty list, no discounts Items = new StripeList { Data = [] } }; sutProvider.GetDependency() .SubscriptionGetAsync( subscriber.GatewaySubscriptionId, Arg.Any()) .Returns(subscription); // Act var result = await sutProvider.Sut.GetSubscriptionAsync(subscriber); // Assert Assert.Null(result.CustomerDiscount); } [Theory] [BitAutoData] public async Task GetSubscriptionAsync_WithMultipleSubscriptionDiscounts_SelectsFirstDiscount( SutProvider sutProvider, User subscriber) { // Arrange - Multiple subscription-level discounts, no customer discount subscriber.Gateway = GatewayType.Stripe; subscriber.GatewayCustomerId = "cus_test123"; subscriber.GatewaySubscriptionId = "sub_test123"; var firstDiscount = new Discount { Coupon = new Coupon { Id = "coupon-10-percent", PercentOff = 10m }, End = null }; var secondDiscount = new Discount { Coupon = new Coupon { Id = "coupon-20-percent", PercentOff = 20m }, End = null }; var subscription = new Subscription { Id = "sub_test123", Status = "active", CollectionMethod = "charge_automatically", Customer = new Customer { Discount = null // No customer discount }, // Multiple subscription discounts - FirstOrDefault() should select the first one Discounts = new List { firstDiscount, secondDiscount }, Items = new StripeList { Data = [] } }; sutProvider.GetDependency() .SubscriptionGetAsync( subscriber.GatewaySubscriptionId, Arg.Any()) .Returns(subscription); // Act var result = await sutProvider.Sut.GetSubscriptionAsync(subscriber); // Assert - Should select the first discount from the list (FirstOrDefault() behavior) Assert.NotNull(result.CustomerDiscount); Assert.Equal("coupon-10-percent", result.CustomerDiscount.Id); Assert.Equal(10m, result.CustomerDiscount.PercentOff); // Verify the second discount was not selected Assert.NotEqual("coupon-20-percent", result.CustomerDiscount.Id); Assert.NotEqual(20m, result.CustomerDiscount.PercentOff); } [Theory] [BitAutoData] public async Task GetSubscriptionAsync_WithNullCustomer_HandlesGracefully( SutProvider sutProvider, User subscriber) { // Arrange - Subscription with null Customer (defensive null check scenario) subscriber.Gateway = GatewayType.Stripe; subscriber.GatewayCustomerId = "cus_test123"; subscriber.GatewaySubscriptionId = "sub_test123"; var subscription = new Subscription { Id = "sub_test123", Status = "active", CollectionMethod = "charge_automatically", Customer = null, // Customer not expanded or null Discounts = new List(), // Empty discounts Items = new StripeList { Data = [] } }; sutProvider.GetDependency() .SubscriptionGetAsync( subscriber.GatewaySubscriptionId, Arg.Any()) .Returns(subscription); // Act var result = await sutProvider.Sut.GetSubscriptionAsync(subscriber); // Assert - Should handle null Customer gracefully without throwing NullReferenceException Assert.Null(result.CustomerDiscount); } [Theory] [BitAutoData] public async Task GetSubscriptionAsync_WithNullDiscounts_HandlesGracefully( SutProvider sutProvider, User subscriber) { // Arrange - Subscription with null Discounts (defensive null check scenario) subscriber.Gateway = GatewayType.Stripe; subscriber.GatewayCustomerId = "cus_test123"; subscriber.GatewaySubscriptionId = "sub_test123"; var subscription = new Subscription { Id = "sub_test123", Status = "active", CollectionMethod = "charge_automatically", Customer = new Customer { Discount = null // No customer discount }, Discounts = null, // Discounts not expanded or null Items = new StripeList { Data = [] } }; sutProvider.GetDependency() .SubscriptionGetAsync( subscriber.GatewaySubscriptionId, Arg.Any()) .Returns(subscription); // Act var result = await sutProvider.Sut.GetSubscriptionAsync(subscriber); // Assert - Should handle null Discounts gracefully without throwing NullReferenceException Assert.Null(result.CustomerDiscount); } [Theory] [BitAutoData] public async Task GetSubscriptionAsync_VerifiesCorrectExpandOptions( SutProvider sutProvider, User subscriber) { // Arrange subscriber.Gateway = GatewayType.Stripe; subscriber.GatewayCustomerId = "cus_test123"; subscriber.GatewaySubscriptionId = "sub_test123"; var subscription = new Subscription { Id = "sub_test123", Status = "active", CollectionMethod = "charge_automatically", Customer = new Customer { Discount = null }, Discounts = new List(), // Empty list Items = new StripeList { Data = [] } }; var stripeAdapter = sutProvider.GetDependency(); stripeAdapter .SubscriptionGetAsync( Arg.Any(), Arg.Any()) .Returns(subscription); // Act await sutProvider.Sut.GetSubscriptionAsync(subscriber); // Assert - Verify expand options are correct await stripeAdapter.Received(1).SubscriptionGetAsync( subscriber.GatewaySubscriptionId, Arg.Is(o => o.Expand.Contains("customer.discount.coupon.applies_to") && o.Expand.Contains("discounts.coupon.applies_to") && o.Expand.Contains("test_clock"))); } [Theory] [BitAutoData] public async Task GetSubscriptionAsync_WithEmptyGatewaySubscriptionId_ReturnsEmptySubscriptionInfo( SutProvider sutProvider, User subscriber) { // Arrange subscriber.GatewaySubscriptionId = null; // Act var result = await sutProvider.Sut.GetSubscriptionAsync(subscriber); // Assert Assert.NotNull(result); Assert.Null(result.Subscription); Assert.Null(result.CustomerDiscount); Assert.Null(result.UpcomingInvoice); // Verify no Stripe API calls were made await sutProvider.GetDependency() .DidNotReceive() .SubscriptionGetAsync(Arg.Any(), Arg.Any()); } }