mirror of
https://github.com/bitwarden/server.git
synced 2024-11-21 12:05:42 +01:00
[PM-11728] Upgrade free organizations without Stripe Sources API (#4757)
* Refactor: Update metadata in OrganizationSignup and OrganizationUpgrade This commit moves the IsFromSecretsManagerTrial flag from the OrganizationUpgrade to the OrganizationSignup because it will only be passed in on organization creation. Additionally, it removes the nullable boolean 'provider' flag passed to OrganizationService.SignUpAsync and instead adds that boolean flag to the OrganizationSignup which seems more appropriate. * Introduce OrganizationSale While I'm trying to ingrain a singular model that can be used to purchase or upgrade organizations, I disliked my previously implemented OrganizationSubscriptionPurchase for being a little too wordy and specific. This sale class aligns more closely with the work we need to complete against Stripe and also uses a private constructor so that it can only be created and utilized via an Organiztion and either OrganizationSignup or OrganizationUpgrade object. * Use OrganizationSale in OrganizationBillingService This commit renames the OrganizationBillingService.PurchaseSubscription to Finalize and passes it the OrganizationSale object. It also updates the method so that, if the organization already has a customer, it retrieves that customer instead of automatically trying to create one which we'll need for upgraded free organizations. * Add functionality for free organization upgrade This commit adds an UpdatePaymentMethod to the OrganizationBillingService that will check if a customer exists for the organization and if not, creates one with the updated payment source and tax information. Then, in the UpgradeOrganizationPlanCommand, we can use the OrganizationUpgrade to get an OrganizationSale and finalize it, which will create a subscription using the customer created as part of the payment method update that takes place right before it on the client-side. Additionally, it adds some tax ID backfill logic to SubscriberService.UpdateTaxInformation * (No Logic) Re-order OrganizationBillingService methods alphabetically * (No Logic) Run dotnet format
This commit is contained in:
parent
f2180aa7b7
commit
68b421fa2b
@ -551,7 +551,7 @@ public class ProviderService : IProviderService
|
|||||||
|
|
||||||
var (organization, _, defaultCollection) = consolidatedBillingEnabled
|
var (organization, _, defaultCollection) = consolidatedBillingEnabled
|
||||||
? await _organizationService.SignupClientAsync(organizationSignup)
|
? await _organizationService.SignupClientAsync(organizationSignup)
|
||||||
: await _organizationService.SignUpAsync(organizationSignup, true);
|
: await _organizationService.SignUpAsync(organizationSignup);
|
||||||
|
|
||||||
var providerOrganization = new ProviderOrganization
|
var providerOrganization = new ProviderOrganization
|
||||||
{
|
{
|
||||||
|
@ -667,7 +667,7 @@ public class ProviderServiceTests
|
|||||||
|
|
||||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||||
sutProvider.GetDependency<IOrganizationService>().SignUpAsync(organizationSignup, true)
|
sutProvider.GetDependency<IOrganizationService>().SignUpAsync(organizationSignup)
|
||||||
.Returns((organization, null as OrganizationUser, new Collection()));
|
.Returns((organization, null as OrganizationUser, new Collection()));
|
||||||
|
|
||||||
var providerOrganization =
|
var providerOrganization =
|
||||||
@ -775,7 +775,7 @@ public class ProviderServiceTests
|
|||||||
|
|
||||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||||
sutProvider.GetDependency<IOrganizationService>().SignUpAsync(organizationSignup, true)
|
sutProvider.GetDependency<IOrganizationService>().SignUpAsync(organizationSignup)
|
||||||
.Returns((organization, null as OrganizationUser, defaultCollection));
|
.Returns((organization, null as OrganizationUser, defaultCollection));
|
||||||
|
|
||||||
var providerOrganization =
|
var providerOrganization =
|
||||||
|
@ -84,6 +84,7 @@ public class ProviderOrganizationsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
var organizationSignup = model.OrganizationCreateRequest.ToOrganizationSignup(user);
|
var organizationSignup = model.OrganizationCreateRequest.ToOrganizationSignup(user);
|
||||||
|
organizationSignup.IsFromProvider = true;
|
||||||
var result = await _providerService.CreateOrganizationAsync(providerId, organizationSignup, model.ClientOwnerEmail, user);
|
var result = await _providerService.CreateOrganizationAsync(providerId, organizationSignup, model.ClientOwnerEmail, user);
|
||||||
return new ProviderOrganizationResponseModel(result);
|
return new ProviderOrganizationResponseModel(result);
|
||||||
}
|
}
|
||||||
|
@ -182,11 +182,9 @@ public class OrganizationBillingController(
|
|||||||
|
|
||||||
var tokenizedPaymentSource = requestBody.PaymentSource.ToDomain();
|
var tokenizedPaymentSource = requestBody.PaymentSource.ToDomain();
|
||||||
|
|
||||||
await subscriberService.UpdatePaymentSource(organization, tokenizedPaymentSource);
|
|
||||||
|
|
||||||
var taxInformation = requestBody.TaxInformation.ToDomain();
|
var taxInformation = requestBody.TaxInformation.ToDomain();
|
||||||
|
|
||||||
await subscriberService.UpdateTaxInformation(organization, taxInformation);
|
await organizationBillingService.UpdatePaymentMethod(organization, tokenizedPaymentSource, taxInformation);
|
||||||
|
|
||||||
return TypedResults.Ok();
|
return TypedResults.Ok();
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,8 @@ public class ProviderClientsController(
|
|||||||
OwnerKey = requestBody.Key,
|
OwnerKey = requestBody.Key,
|
||||||
PublicKey = requestBody.KeyPair.PublicKey,
|
PublicKey = requestBody.KeyPair.PublicKey,
|
||||||
PrivateKey = requestBody.KeyPair.EncryptedPrivateKey,
|
PrivateKey = requestBody.KeyPair.EncryptedPrivateKey,
|
||||||
CollectionName = requestBody.CollectionName
|
CollectionName = requestBody.CollectionName,
|
||||||
|
IsFromProvider = true
|
||||||
};
|
};
|
||||||
|
|
||||||
var providerOrganization = await providerService.CreateOrganizationAsync(
|
var providerOrganization = await providerService.CreateOrganizationAsync(
|
||||||
|
@ -25,7 +25,7 @@ public interface IOrganizationService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>A tuple containing the new organization, the initial organizationUser (if any) and the default collection (if any)</returns>
|
/// <returns>A tuple containing the new organization, the initial organizationUser (if any) and the default collection (if any)</returns>
|
||||||
#nullable enable
|
#nullable enable
|
||||||
Task<(Organization organization, OrganizationUser? organizationUser, Collection? defaultCollection)> SignUpAsync(OrganizationSignup organizationSignup, bool provider = false);
|
Task<(Organization organization, OrganizationUser? organizationUser, Collection? defaultCollection)> SignUpAsync(OrganizationSignup organizationSignup);
|
||||||
|
|
||||||
Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignupClientAsync(OrganizationSignup signup);
|
Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignupClientAsync(OrganizationSignup signup);
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
@ -15,6 +15,7 @@ using Bit.Core.Auth.Models.Business.Tokenables;
|
|||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Billing.Models.Sales;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
@ -502,8 +503,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new organization in a cloud environment
|
/// Create a new organization in a cloud environment
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignUpAsync(OrganizationSignup signup,
|
public async Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignUpAsync(OrganizationSignup signup)
|
||||||
bool provider = false)
|
|
||||||
{
|
{
|
||||||
var plan = StaticStore.GetPlan(signup.Plan);
|
var plan = StaticStore.GetPlan(signup.Plan);
|
||||||
|
|
||||||
@ -511,7 +511,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
|
|
||||||
if (signup.UseSecretsManager)
|
if (signup.UseSecretsManager)
|
||||||
{
|
{
|
||||||
if (provider)
|
if (signup.IsFromProvider)
|
||||||
{
|
{
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
"Organizations with a Managed Service Provider do not support Secrets Manager.");
|
"Organizations with a Managed Service Provider do not support Secrets Manager.");
|
||||||
@ -519,7 +519,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
ValidateSecretsManagerPlan(plan, signup);
|
ValidateSecretsManagerPlan(plan, signup);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!provider)
|
if (!signup.IsFromProvider)
|
||||||
{
|
{
|
||||||
await ValidateSignUpPoliciesAsync(signup.Owner.Id);
|
await ValidateSignUpPoliciesAsync(signup.Owner.Id);
|
||||||
}
|
}
|
||||||
@ -570,7 +570,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
signup.AdditionalServiceAccounts.GetValueOrDefault();
|
signup.AdditionalServiceAccounts.GetValueOrDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (plan.Type == PlanType.Free && !provider)
|
if (plan.Type == PlanType.Free && !signup.IsFromProvider)
|
||||||
{
|
{
|
||||||
var adminCount =
|
var adminCount =
|
||||||
await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(signup.Owner.Id);
|
await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(signup.Owner.Id);
|
||||||
@ -585,20 +585,19 @@ public class OrganizationService : IOrganizationService
|
|||||||
|
|
||||||
if (deprecateStripeSourcesAPI)
|
if (deprecateStripeSourcesAPI)
|
||||||
{
|
{
|
||||||
var subscriptionPurchase = signup.ToSubscriptionPurchase(provider);
|
var sale = OrganizationSale.From(organization, signup);
|
||||||
|
await _organizationBillingService.Finalize(sale);
|
||||||
await _organizationBillingService.PurchaseSubscription(organization, subscriptionPurchase);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await _paymentService.PurchaseOrganizationAsync(organization, signup.PaymentMethodType.Value,
|
await _paymentService.PurchaseOrganizationAsync(organization, signup.PaymentMethodType.Value,
|
||||||
signup.PaymentToken, plan, signup.AdditionalStorageGb, signup.AdditionalSeats,
|
signup.PaymentToken, plan, signup.AdditionalStorageGb, signup.AdditionalSeats,
|
||||||
signup.PremiumAccessAddon, signup.TaxInfo, provider, signup.AdditionalSmSeats.GetValueOrDefault(),
|
signup.PremiumAccessAddon, signup.TaxInfo, signup.IsFromProvider, signup.AdditionalSmSeats.GetValueOrDefault(),
|
||||||
signup.AdditionalServiceAccounts.GetValueOrDefault(), signup.IsFromSecretsManagerTrial);
|
signup.AdditionalServiceAccounts.GetValueOrDefault(), signup.IsFromSecretsManagerTrial);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var ownerId = provider ? default : signup.Owner.Id;
|
var ownerId = signup.IsFromProvider ? default : signup.Owner.Id;
|
||||||
var returnValue = await SignUpAsync(organization, ownerId, signup.OwnerKey, signup.CollectionName, true);
|
var returnValue = await SignUpAsync(organization, ownerId, signup.OwnerKey, signup.CollectionName, true);
|
||||||
await _referenceEventService.RaiseEventAsync(
|
await _referenceEventService.RaiseEventAsync(
|
||||||
new ReferenceEvent(ReferenceEventType.Signup, organization, _currentContext)
|
new ReferenceEvent(ReferenceEventType.Signup, organization, _currentContext)
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
using Bit.Core.Billing.Enums;
|
|
||||||
|
|
||||||
namespace Bit.Core.Billing.Models;
|
|
||||||
|
|
||||||
public record OrganizationSubscriptionPurchase(
|
|
||||||
OrganizationSubscriptionPurchaseMetadata Metadata,
|
|
||||||
OrganizationPasswordManagerSubscriptionPurchase PasswordManagerSubscription,
|
|
||||||
TokenizedPaymentSource PaymentSource,
|
|
||||||
PlanType PlanType,
|
|
||||||
OrganizationSecretsManagerSubscriptionPurchase SecretsManagerSubscription,
|
|
||||||
TaxInformation TaxInformation);
|
|
||||||
|
|
||||||
public record OrganizationPasswordManagerSubscriptionPurchase(
|
|
||||||
int Storage,
|
|
||||||
bool PremiumAccess,
|
|
||||||
int Seats);
|
|
||||||
|
|
||||||
public record OrganizationSecretsManagerSubscriptionPurchase(
|
|
||||||
int Seats,
|
|
||||||
int ServiceAccounts);
|
|
||||||
|
|
||||||
public record OrganizationSubscriptionPurchaseMetadata(
|
|
||||||
bool FromProvider,
|
|
||||||
bool FromSecretsManagerStandalone)
|
|
||||||
{
|
|
||||||
public static OrganizationSubscriptionPurchaseMetadata Default => new(false, false);
|
|
||||||
}
|
|
10
src/Core/Billing/Models/Sales/CustomerSetup.cs
Normal file
10
src/Core/Billing/Models/Sales/CustomerSetup.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
namespace Bit.Core.Billing.Models.Sales;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
public class CustomerSetup
|
||||||
|
{
|
||||||
|
public required TokenizedPaymentSource TokenizedPaymentSource { get; set; }
|
||||||
|
public required TaxInformation TaxInformation { get; set; }
|
||||||
|
public string? Coupon { get; set; }
|
||||||
|
}
|
104
src/Core/Billing/Models/Sales/OrganizationSale.cs
Normal file
104
src/Core/Billing/Models/Sales/OrganizationSale.cs
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.Billing.Constants;
|
||||||
|
using Bit.Core.Models.Business;
|
||||||
|
|
||||||
|
namespace Bit.Core.Billing.Models.Sales;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
public class OrganizationSale
|
||||||
|
{
|
||||||
|
private OrganizationSale() { }
|
||||||
|
|
||||||
|
public void Deconstruct(
|
||||||
|
out Organization organization,
|
||||||
|
out CustomerSetup? customerSetup,
|
||||||
|
out SubscriptionSetup subscriptionSetup)
|
||||||
|
{
|
||||||
|
organization = Organization;
|
||||||
|
customerSetup = CustomerSetup;
|
||||||
|
subscriptionSetup = SubscriptionSetup;
|
||||||
|
}
|
||||||
|
|
||||||
|
public required Organization Organization { get; init; }
|
||||||
|
public CustomerSetup? CustomerSetup { get; init; }
|
||||||
|
public required SubscriptionSetup SubscriptionSetup { get; init; }
|
||||||
|
|
||||||
|
public static OrganizationSale From(
|
||||||
|
Organization organization,
|
||||||
|
OrganizationSignup signup) => new()
|
||||||
|
{
|
||||||
|
Organization = organization,
|
||||||
|
CustomerSetup = string.IsNullOrEmpty(organization.GatewayCustomerId) ? GetCustomerSetup(signup) : null,
|
||||||
|
SubscriptionSetup = GetSubscriptionSetup(signup)
|
||||||
|
};
|
||||||
|
|
||||||
|
public static OrganizationSale From(
|
||||||
|
Organization organization,
|
||||||
|
OrganizationUpgrade upgrade) => new()
|
||||||
|
{
|
||||||
|
Organization = organization,
|
||||||
|
SubscriptionSetup = GetSubscriptionSetup(upgrade)
|
||||||
|
};
|
||||||
|
|
||||||
|
private static CustomerSetup? GetCustomerSetup(OrganizationSignup signup)
|
||||||
|
{
|
||||||
|
if (!signup.PaymentMethodType.HasValue)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tokenizedPaymentSource = new TokenizedPaymentSource(
|
||||||
|
signup.PaymentMethodType!.Value,
|
||||||
|
signup.PaymentToken);
|
||||||
|
|
||||||
|
var taxInformation = new TaxInformation(
|
||||||
|
signup.TaxInfo.BillingAddressCountry,
|
||||||
|
signup.TaxInfo.BillingAddressPostalCode,
|
||||||
|
signup.TaxInfo.TaxIdNumber,
|
||||||
|
signup.TaxInfo.BillingAddressLine1,
|
||||||
|
signup.TaxInfo.BillingAddressLine2,
|
||||||
|
signup.TaxInfo.BillingAddressCity,
|
||||||
|
signup.TaxInfo.BillingAddressState);
|
||||||
|
|
||||||
|
var coupon = signup.IsFromProvider
|
||||||
|
? StripeConstants.CouponIDs.MSPDiscount35
|
||||||
|
: signup.IsFromSecretsManagerTrial
|
||||||
|
? StripeConstants.CouponIDs.SecretsManagerStandalone
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return new CustomerSetup
|
||||||
|
{
|
||||||
|
TokenizedPaymentSource = tokenizedPaymentSource,
|
||||||
|
TaxInformation = taxInformation,
|
||||||
|
Coupon = coupon
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SubscriptionSetup GetSubscriptionSetup(OrganizationUpgrade upgrade)
|
||||||
|
{
|
||||||
|
var plan = Core.Utilities.StaticStore.GetPlan(upgrade.Plan);
|
||||||
|
|
||||||
|
var passwordManagerOptions = new SubscriptionSetup.PasswordManager
|
||||||
|
{
|
||||||
|
Seats = upgrade.AdditionalSeats,
|
||||||
|
Storage = upgrade.AdditionalStorageGb,
|
||||||
|
PremiumAccess = upgrade.PremiumAccessAddon
|
||||||
|
};
|
||||||
|
|
||||||
|
var secretsManagerOptions = upgrade.UseSecretsManager
|
||||||
|
? new SubscriptionSetup.SecretsManager
|
||||||
|
{
|
||||||
|
Seats = upgrade.AdditionalSmSeats ?? 0,
|
||||||
|
ServiceAccounts = upgrade.AdditionalServiceAccounts
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return new SubscriptionSetup
|
||||||
|
{
|
||||||
|
Plan = plan,
|
||||||
|
PasswordManagerOptions = passwordManagerOptions,
|
||||||
|
SecretsManagerOptions = secretsManagerOptions
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
25
src/Core/Billing/Models/Sales/SubscriptionSetup.cs
Normal file
25
src/Core/Billing/Models/Sales/SubscriptionSetup.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
using Bit.Core.Models.StaticStore;
|
||||||
|
|
||||||
|
namespace Bit.Core.Billing.Models.Sales;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
public class SubscriptionSetup
|
||||||
|
{
|
||||||
|
public required Plan Plan { get; set; }
|
||||||
|
public required PasswordManager PasswordManagerOptions { get; set; }
|
||||||
|
public SecretsManager? SecretsManagerOptions { get; set; }
|
||||||
|
|
||||||
|
public class PasswordManager
|
||||||
|
{
|
||||||
|
public required int Seats { get; set; }
|
||||||
|
public short? Storage { get; set; }
|
||||||
|
public bool? PremiumAccess { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SecretsManager
|
||||||
|
{
|
||||||
|
public required int Seats { get; set; }
|
||||||
|
public int? ServiceAccounts { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -34,6 +34,9 @@ public abstract record Plan
|
|||||||
public SecretsManagerPlanFeatures SecretsManager { get; protected init; }
|
public SecretsManagerPlanFeatures SecretsManager { get; protected init; }
|
||||||
public bool SupportsSecretsManager => SecretsManager != null;
|
public bool SupportsSecretsManager => SecretsManager != null;
|
||||||
|
|
||||||
|
public bool HasNonSeatBasedPasswordManagerPlan() =>
|
||||||
|
PasswordManager is { StripePlanId: not null and not "", StripeSeatPlanId: null or "" };
|
||||||
|
|
||||||
public record SecretsManagerPlanFeatures
|
public record SecretsManagerPlanFeatures
|
||||||
{
|
{
|
||||||
// Service accounts
|
// Service accounts
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
namespace Bit.Core.Billing.Models;
|
using Stripe;
|
||||||
|
|
||||||
|
namespace Bit.Core.Billing.Models;
|
||||||
|
|
||||||
public record TaxInformation(
|
public record TaxInformation(
|
||||||
string Country,
|
string Country,
|
||||||
@ -9,6 +11,25 @@ public record TaxInformation(
|
|||||||
string City,
|
string City,
|
||||||
string State)
|
string State)
|
||||||
{
|
{
|
||||||
|
public (AddressOptions, List<CustomerTaxIdDataOptions>) GetStripeOptions()
|
||||||
|
{
|
||||||
|
var address = new AddressOptions
|
||||||
|
{
|
||||||
|
Country = Country,
|
||||||
|
PostalCode = PostalCode,
|
||||||
|
Line1 = Line1,
|
||||||
|
Line2 = Line2,
|
||||||
|
City = City,
|
||||||
|
State = State
|
||||||
|
};
|
||||||
|
|
||||||
|
var customerTaxIdDataOptionsList = !string.IsNullOrEmpty(TaxId)
|
||||||
|
? new List<CustomerTaxIdDataOptions> { new() { Type = GetTaxIdType(), Value = TaxId } }
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return (address, customerTaxIdDataOptionsList);
|
||||||
|
}
|
||||||
|
|
||||||
public string GetTaxIdType()
|
public string GetTaxIdType()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(Country) || string.IsNullOrEmpty(TaxId))
|
if (string.IsNullOrEmpty(Country) || string.IsNullOrEmpty(TaxId))
|
||||||
|
@ -1,10 +1,29 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
|
using Bit.Core.Billing.Models.Sales;
|
||||||
|
|
||||||
namespace Bit.Core.Billing.Services;
|
namespace Bit.Core.Billing.Services;
|
||||||
|
|
||||||
public interface IOrganizationBillingService
|
public interface IOrganizationBillingService
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// <para>Establishes the billing configuration for a Bitwarden <see cref="Organization"/> using the provided <paramref name="sale"/>.</para>
|
||||||
|
/// <para>
|
||||||
|
/// The method first checks to see if the
|
||||||
|
/// provided <see cref="OrganizationSale.Organization"/> already has a Stripe <see cref="Stripe.Customer"/> using the <see cref="Organization.GatewayCustomerId"/>.
|
||||||
|
/// If it doesn't, the method creates one using the <paramref name="sale"/>'s <see cref="OrganizationSale.CustomerSetup"/>. The method then creates a Stripe <see cref="Stripe.Subscription"/>
|
||||||
|
/// for the created or existing customer using the provided <see cref="OrganizationSale.SubscriptionSetup"/>.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sale">The purchase details necessary to establish the Stripe entities responsible for billing the organization.</param>
|
||||||
|
/// <example>
|
||||||
|
/// <code>
|
||||||
|
/// var sale = OrganizationSale.From(organization, organizationSignup);
|
||||||
|
/// await organizationBillingService.Finalize(sale);
|
||||||
|
/// </code>
|
||||||
|
/// </example>
|
||||||
|
Task Finalize(OrganizationSale sale);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieve metadata about the organization represented bsy the provided <paramref name="organizationId"/>.
|
/// Retrieve metadata about the organization represented bsy the provided <paramref name="organizationId"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -13,11 +32,15 @@ public interface IOrganizationBillingService
|
|||||||
Task<OrganizationMetadata> GetMetadata(Guid organizationId);
|
Task<OrganizationMetadata> GetMetadata(Guid organizationId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Purchase a subscription for the provided <paramref name="organization"/> using the provided <paramref name="organizationSubscriptionPurchase"/>.
|
/// Updates the provided <paramref name="organization"/>'s payment source and tax information.
|
||||||
/// If successful, a Stripe <see cref="Stripe.Customer"/> and <see cref="Stripe.Subscription"/> will be created for the organization and the
|
/// If the <paramref name="organization"/> does not have a Stripe <see cref="Stripe.Customer"/>, this method will create one using the provided
|
||||||
/// organization will be enabled.
|
/// <paramref name="tokenizedPaymentSource"/> and <paramref name="taxInformation"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="organization">The organization to purchase a subscription for.</param>
|
/// <param name="organization">The <paramref name="organization"/> to update the payment source and tax information for.</param>
|
||||||
/// <param name="organizationSubscriptionPurchase">The purchase information for the organization's subscription.</param>
|
/// <param name="tokenizedPaymentSource">The tokenized payment source (ex. Credit Card) to attach to the <paramref name="organization"/>.</param>
|
||||||
Task PurchaseSubscription(Organization organization, OrganizationSubscriptionPurchase organizationSubscriptionPurchase);
|
/// <param name="taxInformation">The <paramref name="organization"/>'s updated tax information.</param>
|
||||||
|
Task UpdatePaymentMethod(
|
||||||
|
Organization organization,
|
||||||
|
TokenizedPaymentSource tokenizedPaymentSource,
|
||||||
|
TaxInformation taxInformation);
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.Billing.Caches;
|
using Bit.Core.Billing.Caches;
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Billing.Enums;
|
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
|
using Bit.Core.Billing.Models.Sales;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
@ -28,6 +28,31 @@ public class OrganizationBillingService(
|
|||||||
IStripeAdapter stripeAdapter,
|
IStripeAdapter stripeAdapter,
|
||||||
ISubscriberService subscriberService) : IOrganizationBillingService
|
ISubscriberService subscriberService) : IOrganizationBillingService
|
||||||
{
|
{
|
||||||
|
public async Task Finalize(OrganizationSale sale)
|
||||||
|
{
|
||||||
|
var (organization, customerSetup, subscriptionSetup) = sale;
|
||||||
|
|
||||||
|
List<string> expand = ["tax"];
|
||||||
|
|
||||||
|
var customer = customerSetup != null
|
||||||
|
? await CreateCustomerAsync(organization, customerSetup, expand)
|
||||||
|
: await subscriberService.GetCustomerOrThrow(organization, new CustomerGetOptions { Expand = expand });
|
||||||
|
|
||||||
|
var subscription = await CreateSubscriptionAsync(organization.Id, customer, subscriptionSetup);
|
||||||
|
|
||||||
|
if (subscription.Status is StripeConstants.SubscriptionStatus.Trialing or StripeConstants.SubscriptionStatus.Active)
|
||||||
|
{
|
||||||
|
organization.Enabled = true;
|
||||||
|
organization.ExpirationDate = subscription.CurrentPeriodEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
organization.Gateway = GatewayType.Stripe;
|
||||||
|
organization.GatewayCustomerId = customer.Id;
|
||||||
|
organization.GatewaySubscriptionId = subscription.Id;
|
||||||
|
|
||||||
|
await organizationRepository.ReplaceAsync(organization);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<OrganizationMetadata> GetMetadata(Guid organizationId)
|
public async Task<OrganizationMetadata> GetMetadata(Guid organizationId)
|
||||||
{
|
{
|
||||||
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
||||||
@ -54,97 +79,72 @@ public class OrganizationBillingService(
|
|||||||
return new OrganizationMetadata(isOnSecretsManagerStandalone);
|
return new OrganizationMetadata(isOnSecretsManagerStandalone);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task PurchaseSubscription(
|
public async Task UpdatePaymentMethod(
|
||||||
Organization organization,
|
Organization organization,
|
||||||
OrganizationSubscriptionPurchase organizationSubscriptionPurchase)
|
TokenizedPaymentSource tokenizedPaymentSource,
|
||||||
|
TaxInformation taxInformation)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(organization);
|
if (string.IsNullOrEmpty(organization.GatewayCustomerId))
|
||||||
ArgumentNullException.ThrowIfNull(organizationSubscriptionPurchase);
|
{
|
||||||
|
var customer = await CreateCustomerAsync(organization,
|
||||||
|
new CustomerSetup
|
||||||
|
{
|
||||||
|
TokenizedPaymentSource = tokenizedPaymentSource,
|
||||||
|
TaxInformation = taxInformation,
|
||||||
|
});
|
||||||
|
|
||||||
var (
|
|
||||||
metadata,
|
|
||||||
passwordManager,
|
|
||||||
paymentSource,
|
|
||||||
planType,
|
|
||||||
secretsManager,
|
|
||||||
taxInformation) = organizationSubscriptionPurchase;
|
|
||||||
|
|
||||||
var customer = await CreateCustomerAsync(organization, metadata, paymentSource, taxInformation);
|
|
||||||
|
|
||||||
var subscription =
|
|
||||||
await CreateSubscriptionAsync(customer, organization.Id, passwordManager, planType, secretsManager);
|
|
||||||
|
|
||||||
organization.Enabled = true;
|
|
||||||
organization.ExpirationDate = subscription.CurrentPeriodEnd;
|
|
||||||
organization.Gateway = GatewayType.Stripe;
|
organization.Gateway = GatewayType.Stripe;
|
||||||
organization.GatewayCustomerId = customer.Id;
|
organization.GatewayCustomerId = customer.Id;
|
||||||
organization.GatewaySubscriptionId = subscription.Id;
|
|
||||||
|
|
||||||
await organizationRepository.ReplaceAsync(organization);
|
await organizationRepository.ReplaceAsync(organization);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await subscriberService.UpdatePaymentSource(organization, tokenizedPaymentSource);
|
||||||
|
await subscriberService.UpdateTaxInformation(organization, taxInformation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#region Utilities
|
#region Utilities
|
||||||
|
|
||||||
private async Task<Customer> CreateCustomerAsync(
|
private async Task<Customer> CreateCustomerAsync(
|
||||||
Organization organization,
|
Organization organization,
|
||||||
OrganizationSubscriptionPurchaseMetadata metadata,
|
CustomerSetup customerSetup,
|
||||||
TokenizedPaymentSource paymentSource,
|
List<string> expand = null)
|
||||||
TaxInformation taxInformation)
|
|
||||||
{
|
{
|
||||||
if (paymentSource == null)
|
if (customerSetup.TokenizedPaymentSource is not
|
||||||
|
{
|
||||||
|
Type: PaymentMethodType.BankAccount or PaymentMethodType.Card or PaymentMethodType.PayPal,
|
||||||
|
Token: not null and not ""
|
||||||
|
})
|
||||||
{
|
{
|
||||||
logger.LogError(
|
logger.LogError(
|
||||||
"Cannot create customer for organization ({OrganizationID}) without a payment source",
|
"Cannot create customer for organization ({OrganizationID}) without a valid payment source",
|
||||||
organization.Id);
|
organization.Id);
|
||||||
|
|
||||||
throw new BillingException();
|
throw new BillingException();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (taxInformation is not { Country: not null, PostalCode: not null })
|
if (customerSetup.TaxInformation is not { Country: not null and not "", PostalCode: not null and not "" })
|
||||||
{
|
{
|
||||||
logger.LogError(
|
logger.LogError(
|
||||||
"Cannot create customer for organization ({OrganizationID}) without both a country and postal code",
|
"Cannot create customer for organization ({OrganizationID}) without valid tax information",
|
||||||
organization.Id);
|
organization.Id);
|
||||||
|
|
||||||
throw new BillingException();
|
throw new BillingException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (address, taxIdData) = customerSetup.TaxInformation.GetStripeOptions();
|
||||||
country,
|
|
||||||
postalCode,
|
|
||||||
taxId,
|
|
||||||
line1,
|
|
||||||
line2,
|
|
||||||
city,
|
|
||||||
state) = taxInformation;
|
|
||||||
|
|
||||||
var address = new AddressOptions
|
|
||||||
{
|
|
||||||
Country = country,
|
|
||||||
PostalCode = postalCode,
|
|
||||||
City = city,
|
|
||||||
Line1 = line1,
|
|
||||||
Line2 = line2,
|
|
||||||
State = state
|
|
||||||
};
|
|
||||||
|
|
||||||
var (fromProvider, fromSecretsManagerStandalone) = metadata ?? OrganizationSubscriptionPurchaseMetadata.Default;
|
|
||||||
|
|
||||||
var coupon = fromProvider
|
|
||||||
? StripeConstants.CouponIDs.MSPDiscount35
|
|
||||||
: fromSecretsManagerStandalone
|
|
||||||
? StripeConstants.CouponIDs.SecretsManagerStandalone
|
|
||||||
: null;
|
|
||||||
|
|
||||||
var organizationDisplayName = organization.DisplayName();
|
var organizationDisplayName = organization.DisplayName();
|
||||||
|
|
||||||
var customerCreateOptions = new CustomerCreateOptions
|
var customerCreateOptions = new CustomerCreateOptions
|
||||||
{
|
{
|
||||||
Address = address,
|
Address = address,
|
||||||
Coupon = coupon,
|
Coupon = customerSetup.Coupon,
|
||||||
Description = organization.DisplayBusinessName(),
|
Description = organization.DisplayBusinessName(),
|
||||||
Email = organization.BillingEmail,
|
Email = organization.BillingEmail,
|
||||||
Expand = ["tax"],
|
Expand = expand,
|
||||||
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
||||||
{
|
{
|
||||||
CustomFields = [
|
CustomFields = [
|
||||||
@ -164,21 +164,10 @@ public class OrganizationBillingService(
|
|||||||
{
|
{
|
||||||
ValidateLocation = StripeConstants.ValidateTaxLocationTiming.Immediately
|
ValidateLocation = StripeConstants.ValidateTaxLocationTiming.Immediately
|
||||||
},
|
},
|
||||||
TaxIdData = !string.IsNullOrEmpty(taxId)
|
TaxIdData = taxIdData
|
||||||
? [new CustomerTaxIdDataOptions { Type = taxInformation.GetTaxIdType(), Value = taxId }]
|
|
||||||
: null
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var (type, token) = paymentSource;
|
var (type, token) = customerSetup.TokenizedPaymentSource;
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(token))
|
|
||||||
{
|
|
||||||
logger.LogError(
|
|
||||||
"Cannot create customer for organization ({OrganizationID}) without a payment source token",
|
|
||||||
organization.Id);
|
|
||||||
|
|
||||||
throw new BillingException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var braintreeCustomerId = "";
|
var braintreeCustomerId = "";
|
||||||
|
|
||||||
@ -270,31 +259,30 @@ public class OrganizationBillingService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Subscription> CreateSubscriptionAsync(
|
private async Task<Subscription> CreateSubscriptionAsync(
|
||||||
Customer customer,
|
|
||||||
Guid organizationId,
|
Guid organizationId,
|
||||||
OrganizationPasswordManagerSubscriptionPurchase passwordManager,
|
Customer customer,
|
||||||
PlanType planType,
|
SubscriptionSetup subscriptionSetup)
|
||||||
OrganizationSecretsManagerSubscriptionPurchase secretsManager)
|
|
||||||
{
|
{
|
||||||
var plan = StaticStore.GetPlan(planType);
|
var plan = subscriptionSetup.Plan;
|
||||||
|
|
||||||
if (passwordManager == null)
|
var passwordManagerOptions = subscriptionSetup.PasswordManagerOptions;
|
||||||
{
|
|
||||||
logger.LogError("Cannot create subscription for organization ({OrganizationID}) without password manager purchase information", organizationId);
|
|
||||||
|
|
||||||
throw new BillingException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var subscriptionItemOptionsList = new List<SubscriptionItemOptions>
|
var subscriptionItemOptionsList = new List<SubscriptionItemOptions>
|
||||||
{
|
{
|
||||||
new ()
|
plan.HasNonSeatBasedPasswordManagerPlan()
|
||||||
|
? new SubscriptionItemOptions
|
||||||
|
{
|
||||||
|
Price = plan.PasswordManager.StripePlanId,
|
||||||
|
Quantity = 1
|
||||||
|
}
|
||||||
|
: new SubscriptionItemOptions
|
||||||
{
|
{
|
||||||
Price = plan.PasswordManager.StripeSeatPlanId,
|
Price = plan.PasswordManager.StripeSeatPlanId,
|
||||||
Quantity = passwordManager.Seats
|
Quantity = passwordManagerOptions.Seats
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (passwordManager.PremiumAccess)
|
if (passwordManagerOptions.PremiumAccess is true)
|
||||||
{
|
{
|
||||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||||
{
|
{
|
||||||
@ -303,29 +291,31 @@ public class OrganizationBillingService(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (passwordManager.Storage > 0)
|
if (passwordManagerOptions.Storage is > 0)
|
||||||
{
|
{
|
||||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||||
{
|
{
|
||||||
Price = plan.PasswordManager.StripeStoragePlanId,
|
Price = plan.PasswordManager.StripeStoragePlanId,
|
||||||
Quantity = passwordManager.Storage
|
Quantity = passwordManagerOptions.Storage
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (secretsManager != null)
|
var secretsManagerOptions = subscriptionSetup.SecretsManagerOptions;
|
||||||
|
|
||||||
|
if (secretsManagerOptions != null)
|
||||||
{
|
{
|
||||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||||
{
|
{
|
||||||
Price = plan.SecretsManager.StripeSeatPlanId,
|
Price = plan.SecretsManager.StripeSeatPlanId,
|
||||||
Quantity = secretsManager.Seats
|
Quantity = secretsManagerOptions.Seats
|
||||||
});
|
});
|
||||||
|
|
||||||
if (secretsManager.ServiceAccounts > 0)
|
if (secretsManagerOptions.ServiceAccounts is > 0)
|
||||||
{
|
{
|
||||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||||
{
|
{
|
||||||
Price = plan.SecretsManager.StripeServiceAccountPlanId,
|
Price = plan.SecretsManager.StripeServiceAccountPlanId,
|
||||||
Quantity = secretsManager.ServiceAccounts
|
Quantity = secretsManagerOptions.ServiceAccounts
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -347,16 +337,8 @@ public class OrganizationBillingService(
|
|||||||
TrialPeriodDays = plan.TrialPeriodDays,
|
TrialPeriodDays = plan.TrialPeriodDays,
|
||||||
};
|
};
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
|
return await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
|
||||||
}
|
}
|
||||||
catch
|
|
||||||
{
|
|
||||||
await stripeAdapter.CustomerDeleteAsync(customer.Id);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsOnSecretsManagerStandalone(
|
private static bool IsOnSecretsManagerStandalone(
|
||||||
Organization organization,
|
Organization organization,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.Billing.Caches;
|
using Bit.Core.Billing.Caches;
|
||||||
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -585,7 +586,7 @@ public class SubscriberService(
|
|||||||
|
|
||||||
var customer = await GetCustomerOrThrow(subscriber, new CustomerGetOptions
|
var customer = await GetCustomerOrThrow(subscriber, new CustomerGetOptions
|
||||||
{
|
{
|
||||||
Expand = ["tax_ids"]
|
Expand = ["subscriptions", "tax", "tax_ids"]
|
||||||
});
|
});
|
||||||
|
|
||||||
await stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
|
await stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
|
||||||
@ -622,6 +623,23 @@ public class SubscriberService(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (SubscriberIsEligibleForAutomaticTax(subscriber, customer))
|
||||||
|
{
|
||||||
|
await stripeAdapter.SubscriptionUpdateAsync(subscriber.GatewaySubscriptionId,
|
||||||
|
new SubscriptionUpdateOptions
|
||||||
|
{
|
||||||
|
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true },
|
||||||
|
DefaultTaxRates = []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
bool SubscriberIsEligibleForAutomaticTax(ISubscriber localSubscriber, Customer localCustomer)
|
||||||
|
=> !string.IsNullOrEmpty(localSubscriber.GatewaySubscriptionId) &&
|
||||||
|
(localCustomer.Subscriptions?.Any(sub => sub.Id == localSubscriber.GatewaySubscriptionId && !sub.AutomaticTax.Enabled) ?? false) &&
|
||||||
|
localCustomer.Tax?.AutomaticTax == StripeConstants.AutomaticTaxStatus.Supported;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task VerifyBankAccount(
|
public async Task VerifyBankAccount(
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Entities;
|
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
namespace Bit.Core.Models.Business;
|
namespace Bit.Core.Models.Business;
|
||||||
@ -15,42 +14,6 @@ public class OrganizationSignup : OrganizationUpgrade
|
|||||||
public string PaymentToken { get; set; }
|
public string PaymentToken { get; set; }
|
||||||
public int? MaxAutoscaleSeats { get; set; } = null;
|
public int? MaxAutoscaleSeats { get; set; } = null;
|
||||||
public string InitiationPath { get; set; }
|
public string InitiationPath { get; set; }
|
||||||
|
public bool IsFromSecretsManagerTrial { get; set; }
|
||||||
public OrganizationSubscriptionPurchase ToSubscriptionPurchase(bool fromProvider = false)
|
public bool IsFromProvider { get; set; }
|
||||||
{
|
|
||||||
if (!PaymentMethodType.HasValue)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var metadata = new OrganizationSubscriptionPurchaseMetadata(fromProvider, IsFromSecretsManagerTrial);
|
|
||||||
|
|
||||||
var passwordManager = new OrganizationPasswordManagerSubscriptionPurchase(
|
|
||||||
AdditionalStorageGb,
|
|
||||||
PremiumAccessAddon,
|
|
||||||
AdditionalSeats);
|
|
||||||
|
|
||||||
var paymentSource = new TokenizedPaymentSource(PaymentMethodType.Value, PaymentToken);
|
|
||||||
|
|
||||||
var secretsManager = new OrganizationSecretsManagerSubscriptionPurchase(
|
|
||||||
AdditionalSmSeats ?? 0,
|
|
||||||
AdditionalServiceAccounts ?? 0);
|
|
||||||
|
|
||||||
var taxInformation = new TaxInformation(
|
|
||||||
TaxInfo.BillingAddressCountry,
|
|
||||||
TaxInfo.BillingAddressPostalCode,
|
|
||||||
TaxInfo.TaxIdNumber,
|
|
||||||
TaxInfo.BillingAddressLine1,
|
|
||||||
TaxInfo.BillingAddressLine2,
|
|
||||||
TaxInfo.BillingAddressCity,
|
|
||||||
TaxInfo.BillingAddressState);
|
|
||||||
|
|
||||||
return new OrganizationSubscriptionPurchase(
|
|
||||||
metadata,
|
|
||||||
passwordManager,
|
|
||||||
paymentSource,
|
|
||||||
Plan,
|
|
||||||
UseSecretsManager ? secretsManager : null,
|
|
||||||
taxInformation);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -15,5 +15,4 @@ public class OrganizationUpgrade
|
|||||||
public int? AdditionalSmSeats { get; set; }
|
public int? AdditionalSmSeats { get; set; }
|
||||||
public int? AdditionalServiceAccounts { get; set; }
|
public int? AdditionalServiceAccounts { get; set; }
|
||||||
public bool UseSecretsManager { get; set; }
|
public bool UseSecretsManager { get; set; }
|
||||||
public bool IsFromSecretsManagerTrial { get; set; }
|
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,8 @@ using Bit.Core.AdminConsole.Repositories;
|
|||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Billing.Models.Sales;
|
||||||
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
@ -34,6 +36,8 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
|
|||||||
private readonly IServiceAccountRepository _serviceAccountRepository;
|
private readonly IServiceAccountRepository _serviceAccountRepository;
|
||||||
private readonly IOrganizationRepository _organizationRepository;
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
private readonly IOrganizationService _organizationService;
|
private readonly IOrganizationService _organizationService;
|
||||||
|
private readonly IFeatureService _featureService;
|
||||||
|
private readonly IOrganizationBillingService _organizationBillingService;
|
||||||
|
|
||||||
public UpgradeOrganizationPlanCommand(
|
public UpgradeOrganizationPlanCommand(
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
@ -47,7 +51,9 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
|
|||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
IServiceAccountRepository serviceAccountRepository,
|
IServiceAccountRepository serviceAccountRepository,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IOrganizationService organizationService)
|
IOrganizationService organizationService,
|
||||||
|
IFeatureService featureService,
|
||||||
|
IOrganizationBillingService organizationBillingService)
|
||||||
{
|
{
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
_collectionRepository = collectionRepository;
|
_collectionRepository = collectionRepository;
|
||||||
@ -61,6 +67,8 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
|
|||||||
_serviceAccountRepository = serviceAccountRepository;
|
_serviceAccountRepository = serviceAccountRepository;
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_organizationService = organizationService;
|
_organizationService = organizationService;
|
||||||
|
_featureService = featureService;
|
||||||
|
_organizationBillingService = organizationBillingService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Tuple<bool, string>> UpgradePlanAsync(Guid organizationId, OrganizationUpgrade upgrade)
|
public async Task<Tuple<bool, string>> UpgradePlanAsync(Guid organizationId, OrganizationUpgrade upgrade)
|
||||||
@ -215,11 +223,19 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
|
|||||||
var success = true;
|
var success = true;
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
|
if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
|
||||||
|
{
|
||||||
|
if (_featureService.IsEnabled(FeatureFlagKeys.AC2476_DeprecateStripeSourcesAPI))
|
||||||
|
{
|
||||||
|
var sale = OrganizationSale.From(organization, upgrade);
|
||||||
|
await _organizationBillingService.Finalize(sale);
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
paymentIntentClientSecret = await _paymentService.UpgradeFreeOrganizationAsync(organization,
|
paymentIntentClientSecret = await _paymentService.UpgradeFreeOrganizationAsync(organization,
|
||||||
newPlan, upgrade);
|
newPlan, upgrade);
|
||||||
success = string.IsNullOrWhiteSpace(paymentIntentClientSecret);
|
success = string.IsNullOrWhiteSpace(paymentIntentClientSecret);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
paymentIntentClientSecret = await _paymentService.AdjustSubscription(
|
paymentIntentClientSecret = await _paymentService.AdjustSubscription(
|
||||||
|
@ -354,8 +354,9 @@ public class OrganizationServiceTests
|
|||||||
signup.AdditionalServiceAccounts = 20;
|
signup.AdditionalServiceAccounts = 20;
|
||||||
signup.PaymentMethodType = PaymentMethodType.Card;
|
signup.PaymentMethodType = PaymentMethodType.Card;
|
||||||
signup.PremiumAccessAddon = false;
|
signup.PremiumAccessAddon = false;
|
||||||
|
signup.IsFromProvider = true;
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.SignUpAsync(signup, true));
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.SignUpAsync(signup));
|
||||||
Assert.Contains("Organizations with a Managed Service Provider do not support Secrets Manager.", exception.Message);
|
Assert.Contains("Organizations with a Managed Service Provider do not support Secrets Manager.", exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user