1
0
mirror of https://github.com/bitwarden/server synced 2026-01-06 10:34:01 +00:00

[PM 18701]Optional payment modal after signup (#6014)

* Add endpoint to swap plan frequency

* Add endpoint to swap plan frequency

* Resolve pr comments

Signed-off-by: Cy Okeke <cokeke@bitwarden.com>

* Refactor the code

Signed-off-by: Cy Okeke <cokeke@bitwarden.com>

* Refactor for  thr update change frequency

* Add Automatic modal opening

* catch for organization paying with PayPal

---------

Signed-off-by: Cy Okeke <cokeke@bitwarden.com>
This commit is contained in:
cyprain-okeke
2025-07-22 15:57:58 +01:00
committed by GitHub
parent f4e1e2f1f7
commit 8a5823bff7
6 changed files with 143 additions and 0 deletions

View File

@@ -118,4 +118,11 @@ public static class StripeConstants
public const string Deferred = "deferred";
public const string Immediately = "immediately";
}
public static class MissingPaymentMethodBehaviorOptions
{
public const string CreateInvoice = "create_invoice";
public const string Cancel = "cancel";
public const string Pause = "pause";
}
}

View File

@@ -1,4 +1,5 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Organizations.Models;
using Bit.Core.Billing.Tax.Models;
@@ -44,4 +45,15 @@ public interface IOrganizationBillingService
Organization organization,
TokenizedPaymentSource tokenizedPaymentSource,
TaxInformation taxInformation);
/// <summary>
/// Updates the subscription with new plan frequencies and changes the collection method to charge_automatically if a valid payment method exists.
/// Validates that the customer has a payment method attached before switching to automatic charging.
/// Handles both Password Manager and Secrets Manager subscription items separately to ensure billing interval compatibility.
/// </summary>
/// <param name="organization">The Organization whose subscription to update.</param>
/// <param name="newPlanType">The Stripe price/plan for the new Password Manager and secrets manager.</param>
/// <exception cref="ArgumentNullException">Thrown when the <paramref name="organization"/> is <see langword="null"/>.</exception>
/// <exception cref="BillingException">Thrown when no payment method is found for the customer, no plan IDs are provided, or subscription update fails.</exception>
Task UpdateSubscriptionPlanFrequency(Organization organization, PlanType newPlanType);
}

View File

@@ -145,6 +145,55 @@ public class OrganizationBillingService(
{
await subscriberService.UpdatePaymentSource(organization, tokenizedPaymentSource);
await subscriberService.UpdateTaxInformation(organization, taxInformation);
await UpdateMissingPaymentMethodBehaviourAsync(organization);
}
}
public async Task UpdateSubscriptionPlanFrequency(
Organization organization, PlanType newPlanType)
{
ArgumentNullException.ThrowIfNull(organization);
var subscription = await subscriberService.GetSubscriptionOrThrow(organization);
var subscriptionItems = subscription.Items.Data;
var newPlan = await pricingClient.GetPlanOrThrow(newPlanType);
var oldPlan = await pricingClient.GetPlanOrThrow(organization.PlanType);
// Build the subscription update options
var subscriptionItemOptions = new List<SubscriptionItemOptions>();
foreach (var item in subscriptionItems)
{
var subscriptionItemOption = new SubscriptionItemOptions
{
Id = item.Id,
Quantity = item.Quantity,
Price = item.Price.Id == oldPlan.SecretsManager.StripeSeatPlanId ? newPlan.SecretsManager.StripeSeatPlanId : newPlan.PasswordManager.StripeSeatPlanId
};
subscriptionItemOptions.Add(subscriptionItemOption);
}
var updateOptions = new SubscriptionUpdateOptions
{
Items = subscriptionItemOptions,
ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations
};
try
{
// Update the subscription in Stripe
await stripeAdapter.SubscriptionUpdateAsync(subscription.Id, updateOptions);
organization.PlanType = newPlan.Type;
await organizationRepository.ReplaceAsync(organization);
}
catch (StripeException stripeException)
{
logger.LogError(stripeException, "Failed to update subscription plan for subscriber ({SubscriberID}): {Error}",
organization.Id, stripeException.Message);
throw new BillingException(
message: "An error occurred while updating the subscription plan",
innerException: stripeException);
}
}
@@ -545,5 +594,24 @@ public class OrganizationBillingService(
return subscriptionProductIds.Intersect(couponAppliesTo ?? []).Any();
}
private async Task UpdateMissingPaymentMethodBehaviourAsync(Organization organization)
{
var subscription = await subscriberService.GetSubscriptionOrThrow(organization);
if (subscription.TrialSettings?.EndBehavior?.MissingPaymentMethod == StripeConstants.MissingPaymentMethodBehaviorOptions.Cancel)
{
var options = new SubscriptionUpdateOptions
{
TrialSettings = new SubscriptionTrialSettingsOptions
{
EndBehavior = new SubscriptionTrialSettingsEndBehaviorOptions
{
MissingPaymentMethod = StripeConstants.MissingPaymentMethodBehaviorOptions.CreateInvoice
}
}
};
await stripeAdapter.SubscriptionUpdateAsync(organization.GatewaySubscriptionId, options);
}
}
#endregion
}