1
0
mirror of https://github.com/bitwarden/server synced 2025-12-22 19:23:45 +00:00

[PM-26692] Count unverified setup intent as payment method during organization subscription creation (#6433)

* Updated check that determines whether org has payment method to include bank account when determining how to set trial_settings

* Run dotnet format
This commit is contained in:
Alex Morask
2025-10-09 13:20:28 -05:00
committed by GitHub
parent 712926996e
commit 34f5ffd981
6 changed files with 339 additions and 54 deletions

View File

@@ -2,11 +2,11 @@
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Caches;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Organizations.Models;
using Bit.Core.Billing.Payment.Queries;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
using Bit.Core.Services;
@@ -30,8 +30,8 @@ public interface IGetOrganizationWarningsQuery
public class GetOrganizationWarningsQuery(
ICurrentContext currentContext,
IHasPaymentMethodQuery hasPaymentMethodQuery,
IProviderRepository providerRepository,
ISetupIntentCache setupIntentCache,
IStripeAdapter stripeAdapter,
ISubscriberService subscriberService) : IGetOrganizationWarningsQuery
{
@@ -81,15 +81,7 @@ public class GetOrganizationWarningsQuery(
return null;
}
var customer = subscription.Customer;
var hasUnverifiedBankAccount = await HasUnverifiedBankAccountAsync(organization);
var hasPaymentMethod =
!string.IsNullOrEmpty(customer.InvoiceSettings.DefaultPaymentMethodId) ||
!string.IsNullOrEmpty(customer.DefaultSourceId) ||
hasUnverifiedBankAccount ||
customer.Metadata.ContainsKey(MetadataKeys.BraintreeCustomerId);
var hasPaymentMethod = await hasPaymentMethodQuery.Run(organization);
if (hasPaymentMethod)
{
@@ -287,22 +279,4 @@ public class GetOrganizationWarningsQuery(
_ => null
};
}
private async Task<bool> HasUnverifiedBankAccountAsync(
Organization organization)
{
var setupIntentId = await setupIntentCache.GetSetupIntentIdForSubscriber(organization.Id);
if (string.IsNullOrEmpty(setupIntentId))
{
return false;
}
var setupIntent = await stripeAdapter.SetupIntentGet(setupIntentId, new SetupIntentGetOptions
{
Expand = ["payment_method"]
});
return setupIntent.IsUnverifiedBankAccount();
}
}

View File

@@ -6,6 +6,7 @@ using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Models.Sales;
using Bit.Core.Billing.Organizations.Models;
using Bit.Core.Billing.Payment.Queries;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Tax.Models;
@@ -27,6 +28,7 @@ namespace Bit.Core.Billing.Organizations.Services;
public class OrganizationBillingService(
IBraintreeGateway braintreeGateway,
IGlobalSettings globalSettings,
IHasPaymentMethodQuery hasPaymentMethodQuery,
ILogger<OrganizationBillingService> logger,
IOrganizationRepository organizationRepository,
IPricingClient pricingClient,
@@ -43,7 +45,7 @@ public class OrganizationBillingService(
? await CreateCustomerAsync(organization, customerSetup, subscriptionSetup.PlanType)
: await GetCustomerWhileEnsuringCorrectTaxExemptionAsync(organization, subscriptionSetup);
var subscription = await CreateSubscriptionAsync(organization.Id, customer, subscriptionSetup);
var subscription = await CreateSubscriptionAsync(organization, customer, subscriptionSetup);
if (subscription.Status is StripeConstants.SubscriptionStatus.Trialing or StripeConstants.SubscriptionStatus.Active)
{
@@ -120,8 +122,7 @@ public class OrganizationBillingService(
orgOccupiedSeats.Total);
}
public async Task
UpdatePaymentMethod(
public async Task UpdatePaymentMethod(
Organization organization,
TokenizedPaymentSource tokenizedPaymentSource,
TaxInformation taxInformation)
@@ -397,7 +398,7 @@ public class OrganizationBillingService(
}
private async Task<Subscription> CreateSubscriptionAsync(
Guid organizationId,
Organization organization,
Customer customer,
SubscriptionSetup subscriptionSetup)
{
@@ -465,7 +466,7 @@ public class OrganizationBillingService(
Items = subscriptionItemOptionsList,
Metadata = new Dictionary<string, string>
{
["organizationId"] = organizationId.ToString(),
["organizationId"] = organization.Id.ToString(),
["trialInitiationPath"] = !string.IsNullOrEmpty(subscriptionSetup.InitiationPath) &&
subscriptionSetup.InitiationPath.Contains("trial from marketing website")
? "marketing-initiated"
@@ -475,9 +476,10 @@ public class OrganizationBillingService(
TrialPeriodDays = subscriptionSetup.SkipTrial ? 0 : plan.TrialPeriodDays
};
var hasPaymentMethod = await hasPaymentMethodQuery.Run(organization);
// Only set trial_settings.end_behavior.missing_payment_method to "cancel" if there is no payment method
if (string.IsNullOrEmpty(customer.InvoiceSettings?.DefaultPaymentMethodId) &&
!customer.Metadata.ContainsKey(BraintreeCustomerIdKey))
if (!hasPaymentMethod)
{
subscriptionCreateOptions.TrialSettings = new SubscriptionTrialSettingsOptions
{

View File

@@ -0,0 +1,58 @@
using Bit.Core.Billing.Caches;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Services;
using Bit.Core.Entities;
using Bit.Core.Services;
using Stripe;
namespace Bit.Core.Billing.Payment.Queries;
using static StripeConstants;
public interface IHasPaymentMethodQuery
{
Task<bool> Run(ISubscriber subscriber);
}
public class HasPaymentMethodQuery(
ISetupIntentCache setupIntentCache,
IStripeAdapter stripeAdapter,
ISubscriberService subscriberService) : IHasPaymentMethodQuery
{
public async Task<bool> Run(ISubscriber subscriber)
{
var hasUnverifiedBankAccount = await HasUnverifiedBankAccountAsync(subscriber);
var customer = await subscriberService.GetCustomer(subscriber);
if (customer == null)
{
return hasUnverifiedBankAccount;
}
return
!string.IsNullOrEmpty(customer.InvoiceSettings.DefaultPaymentMethodId) ||
!string.IsNullOrEmpty(customer.DefaultSourceId) ||
hasUnverifiedBankAccount ||
customer.Metadata.ContainsKey(MetadataKeys.BraintreeCustomerId);
}
private async Task<bool> HasUnverifiedBankAccountAsync(
ISubscriber subscriber)
{
var setupIntentId = await setupIntentCache.GetSetupIntentIdForSubscriber(subscriber.Id);
if (string.IsNullOrEmpty(setupIntentId))
{
return false;
}
var setupIntent = await stripeAdapter.SetupIntentGet(setupIntentId, new SetupIntentGetOptions
{
Expand = ["payment_method"]
});
return setupIntent.IsUnverifiedBankAccount();
}
}

View File

@@ -19,5 +19,6 @@ public static class Registrations
services.AddTransient<IGetBillingAddressQuery, GetBillingAddressQuery>();
services.AddTransient<IGetCreditQuery, GetCreditQuery>();
services.AddTransient<IGetPaymentMethodQuery, GetPaymentMethodQuery>();
services.AddTransient<IHasPaymentMethodQuery, HasPaymentMethodQuery>();
}
}