diff --git a/src/Exceptionless.Web/Api/Handlers/OrganizationHandler.cs b/src/Exceptionless.Web/Api/Handlers/OrganizationHandler.cs index 9c910c721..b3fcfd833 100644 --- a/src/Exceptionless.Web/Api/Handlers/OrganizationHandler.cs +++ b/src/Exceptionless.Web/Api/Handlers/OrganizationHandler.cs @@ -438,7 +438,9 @@ public async Task> Handle(ChangeOrganizationPlan messag var subscriptionOptions = new SubscriptionCreateOptions { Customer = customer.Id, - Items = [new SubscriptionItemOptions { Price = model.PlanId }] + Items = [new SubscriptionItemOptions { Price = model.PlanId }], + BillingCycleAnchorConfig = CreateMonthlyBillingCycleAnchorConfig(), + ProrationBehavior = "create_prorations" }; if (isPaymentMethod) @@ -454,8 +456,18 @@ public async Task> Handle(ChangeOrganizationPlan messag } else { - var update = new SubscriptionUpdateOptions { Items = [] }; - var create = new SubscriptionCreateOptions { Customer = organization.StripeCustomerId, Items = [] }; + var update = new SubscriptionUpdateOptions + { + Items = [], + ProrationBehavior = "create_prorations" + }; + var create = new SubscriptionCreateOptions + { + Customer = organization.StripeCustomerId, + Items = [], + BillingCycleAnchorConfig = CreateMonthlyBillingCycleAnchorConfig(), + ProrationBehavior = "create_prorations" + }; bool cardUpdated = false; var customerUpdateOptions = new CustomerUpdateOptions { Description = organization.Name }; @@ -543,6 +555,17 @@ await Task.WhenAll( return new ChangePlanResult { Success = true }; } + private static SubscriptionBillingCycleAnchorConfigOptions CreateMonthlyBillingCycleAnchorConfig() + { + return new SubscriptionBillingCycleAnchorConfigOptions + { + DayOfMonth = 1, + Hour = 0, + Minute = 0, + Second = 0 + }; + } + public async Task> Handle(AddOrganizationUser message) { if (String.IsNullOrEmpty(message.Id) || !message.Context.Request.CanAccessOrganization(message.Id) || String.IsNullOrEmpty(message.Email)) diff --git a/tests/Exceptionless.Tests/Api/Endpoints/OrganizationEndpointTests.cs b/tests/Exceptionless.Tests/Api/Endpoints/OrganizationEndpointTests.cs index 4fddfae77..148d1fdd6 100644 --- a/tests/Exceptionless.Tests/Api/Endpoints/OrganizationEndpointTests.cs +++ b/tests/Exceptionless.Tests/Api/Endpoints/OrganizationEndpointTests.cs @@ -1391,6 +1391,12 @@ public async Task ChangePlanAsync_NewCustomerCreatesStripeCustomerAndSubscriptio var item = Assert.Single(subscription.Items); Assert.Equal(_plans.SmallPlan.Id, item.Price); Assert.Equal("coupon_10", Assert.Single(subscription.Discounts).Coupon); + Assert.Equal("create_prorations", subscription.ProrationBehavior); + Assert.NotNull(subscription.BillingCycleAnchorConfig); + Assert.Equal(1, subscription.BillingCycleAnchorConfig.DayOfMonth); + Assert.Equal(0, subscription.BillingCycleAnchorConfig.Hour); + Assert.Equal(0, subscription.BillingCycleAnchorConfig.Minute); + Assert.Equal(0, subscription.BillingCycleAnchorConfig.Second); var organization = await _organizationRepository.GetByIdAsync(SampleDataService.FREE_ORG_ID); Assert.NotNull(organization); @@ -1444,6 +1450,7 @@ public async Task ChangePlanAsync_ExistingCustomerUpdatesPaymentMethodAndSubscri Assert.Equal("si_active", item.Id); Assert.Equal(_plans.SmallPlan.Id, item.Price); Assert.Equal("coupon_10", Assert.Single(updatedSubscription.Options.Discounts).Coupon); + Assert.Equal("create_prorations", updatedSubscription.Options.ProrationBehavior); Assert.Empty(StripeBillingClient.CreatedSubscriptionOptions); var organization = await _organizationRepository.GetByIdAsync(SampleDataService.FREE_ORG_ID); @@ -1454,6 +1461,40 @@ public async Task ChangePlanAsync_ExistingCustomerUpdatesPaymentMethodAndSubscri Assert.Equal(BillingStatus.Active, organization.BillingStatus); } + [Fact] + public async Task ChangePlanAsync_ExistingCustomerWithoutSubscriptionCreatesMonthAlignedSubscription() + { + await SetStripeCustomerIdAsync(SampleDataService.FREE_ORG_ID, "cus_existing"); + + var result = await WithBillingEnabledAsync(() => + SendRequestAsAsync(r => r + .AsFreeOrganizationUser() + .Post() + .AppendPaths("organizations", SampleDataService.FREE_ORG_ID, "change-plan") + .Content(new ChangePlanRequest + { + PlanId = _plans.SmallPlan.Id, + StripeToken = "pm_card_visa", + Last4 = "4242" + }) + .StatusCodeShouldBeOk() + )); + + Assert.NotNull(result); + Assert.True(result.Success, result.Message); + + var subscription = Assert.Single(StripeBillingClient.CreatedSubscriptionOptions); + Assert.Equal("cus_existing", subscription.Customer); + Assert.Equal(_plans.SmallPlan.Id, Assert.Single(subscription.Items).Price); + Assert.Equal("create_prorations", subscription.ProrationBehavior); + Assert.NotNull(subscription.BillingCycleAnchorConfig); + Assert.Equal(1, subscription.BillingCycleAnchorConfig.DayOfMonth); + Assert.Equal(0, subscription.BillingCycleAnchorConfig.Hour); + Assert.Equal(0, subscription.BillingCycleAnchorConfig.Minute); + Assert.Equal(0, subscription.BillingCycleAnchorConfig.Second); + Assert.Empty(StripeBillingClient.UpdatedSubscriptions); + } + [Fact] public async Task ChangePlanAsync_FreePlanCancelsActiveStripeSubscriptions() {