mirror of
https://github.com/bitwarden/server.git
synced 2024-11-21 12:05:42 +01:00
[AC-2965] Use OrganizationBillingService to purchase org when FF is on (#4737)
* Add PurchaseSubscription to OrganizationBillingService and call from OrganizationService.SignUpAsync when FF is on * Run dotnet format * Missed billing service DI for SCIM which uses the OrganizationService
This commit is contained in:
parent
8491c58595
commit
c0a4ba8de1
@ -1,4 +1,5 @@
|
||||
using System.Globalization;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.SecretsManager.Repositories.Noop;
|
||||
@ -68,6 +69,7 @@ public class Startup
|
||||
// Services
|
||||
services.AddBaseServices(globalSettings);
|
||||
services.AddDefaultServices(globalSettings);
|
||||
services.AddBillingOperations();
|
||||
|
||||
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System.Globalization;
|
||||
using Bit.Billing.Services;
|
||||
using Bit.Billing.Services.Implementations;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.SecretsManager.Repositories.Noop;
|
||||
@ -74,6 +75,7 @@ public class Startup
|
||||
// Services
|
||||
services.AddBaseServices(globalSettings);
|
||||
services.AddDefaultServices(globalSettings);
|
||||
services.AddBillingOperations();
|
||||
|
||||
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||
|
||||
|
@ -15,6 +15,7 @@ using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@ -70,6 +71,7 @@ public class OrganizationService : IOrganizationService
|
||||
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IOrganizationBillingService _organizationBillingService;
|
||||
|
||||
public OrganizationService(
|
||||
IOrganizationRepository organizationRepository,
|
||||
@ -103,7 +105,8 @@ public class OrganizationService : IOrganizationService
|
||||
IDataProtectorTokenFactory<OrgDeleteTokenable> orgDeleteTokenDataFactory,
|
||||
IProviderRepository providerRepository,
|
||||
IFeatureService featureService,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery)
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IOrganizationBillingService organizationBillingService)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
@ -137,6 +140,7 @@ public class OrganizationService : IOrganizationService
|
||||
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
|
||||
_featureService = featureService;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
_organizationBillingService = organizationBillingService;
|
||||
}
|
||||
|
||||
public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken,
|
||||
@ -577,10 +581,21 @@ public class OrganizationService : IOrganizationService
|
||||
}
|
||||
else if (plan.Type != PlanType.Free)
|
||||
{
|
||||
await _paymentService.PurchaseOrganizationAsync(organization, signup.PaymentMethodType.Value,
|
||||
signup.PaymentToken, plan, signup.AdditionalStorageGb, signup.AdditionalSeats,
|
||||
signup.PremiumAccessAddon, signup.TaxInfo, provider, signup.AdditionalSmSeats.GetValueOrDefault(),
|
||||
signup.AdditionalServiceAccounts.GetValueOrDefault(), signup.IsFromSecretsManagerTrial);
|
||||
var deprecateStripeSourcesAPI = _featureService.IsEnabled(FeatureFlagKeys.AC2476_DeprecateStripeSourcesAPI);
|
||||
|
||||
if (deprecateStripeSourcesAPI)
|
||||
{
|
||||
var subscriptionPurchase = signup.ToSubscriptionPurchase(provider);
|
||||
|
||||
await _organizationBillingService.PurchaseSubscription(organization, subscriptionPurchase);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _paymentService.PurchaseOrganizationAsync(organization, signup.PaymentMethodType.Value,
|
||||
signup.PaymentToken, plan, signup.AdditionalStorageGb, signup.AdditionalSeats,
|
||||
signup.PremiumAccessAddon, signup.TaxInfo, provider, signup.AdditionalSmSeats.GetValueOrDefault(),
|
||||
signup.AdditionalServiceAccounts.GetValueOrDefault(), signup.IsFromSecretsManagerTrial);
|
||||
}
|
||||
}
|
||||
|
||||
var ownerId = provider ? default : signup.Owner.Id;
|
||||
|
@ -18,6 +18,7 @@ public static class StripeConstants
|
||||
|
||||
public static class CouponIDs
|
||||
{
|
||||
public const string MSPDiscount35 = "msp-discount-35";
|
||||
public const string SecretsManagerStandalone = "sm-standalone";
|
||||
}
|
||||
|
||||
@ -51,4 +52,10 @@ public static class StripeConstants
|
||||
public const string Unpaid = "unpaid";
|
||||
public const string Paused = "paused";
|
||||
}
|
||||
|
||||
public static class ValidateTaxLocationTiming
|
||||
{
|
||||
public const string Deferred = "deferred";
|
||||
public const string Immediately = "immediately";
|
||||
}
|
||||
}
|
||||
|
27
src/Core/Billing/Models/OrganizationSubscriptionPurchase.cs
Normal file
27
src/Core/Billing/Models/OrganizationSubscriptionPurchase.cs
Normal file
@ -0,0 +1,27 @@
|
||||
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);
|
||||
}
|
@ -1,8 +1,23 @@
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Models;
|
||||
|
||||
namespace Bit.Core.Billing.Services;
|
||||
|
||||
public interface IOrganizationBillingService
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieve metadata about the organization represented bsy the provided <paramref name="organizationId"/>.
|
||||
/// </summary>
|
||||
/// <param name="organizationId">The ID of the organization to retrieve metadata for.</param>
|
||||
/// <returns>An <see cref="OrganizationMetadata"/> record.</returns>
|
||||
Task<OrganizationMetadata> GetMetadata(Guid organizationId);
|
||||
|
||||
/// <summary>
|
||||
/// Purchase a subscription for the provided <paramref name="organization"/> using the provided <paramref name="organizationSubscriptionPurchase"/>.
|
||||
/// If successful, a Stripe <see cref="Stripe.Customer"/> and <see cref="Stripe.Subscription"/> will be created for the organization and the
|
||||
/// organization will be enabled.
|
||||
/// </summary>
|
||||
/// <param name="organization">The organization to purchase a subscription for.</param>
|
||||
/// <param name="organizationSubscriptionPurchase">The purchase information for the organization's subscription.</param>
|
||||
Task PurchaseSubscription(Organization organization, OrganizationSubscriptionPurchase organizationSubscriptionPurchase);
|
||||
}
|
||||
|
@ -22,6 +22,16 @@ public interface ISubscriberService
|
||||
OffboardingSurveyResponse offboardingSurveyResponse,
|
||||
bool cancelImmediately);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Braintree <see cref="Braintree.Customer"/> for the provided <paramref name="subscriber"/> while attaching the provided <paramref name="paymentMethodNonce"/>.
|
||||
/// </summary>
|
||||
/// <param name="subscriber">The subscriber to create a Braintree customer for.</param>
|
||||
/// <param name="paymentMethodNonce">A nonce representing the PayPal payment method the customer will use for payments.</param>
|
||||
/// <returns>The <see cref="Braintree.Customer.Id"/> of the created Braintree customer.</returns>
|
||||
Task<string> CreateBraintreeCustomer(
|
||||
ISubscriber subscriber,
|
||||
string paymentMethodNonce);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a Stripe <see cref="Customer"/> using the <paramref name="subscriber"/>'s <see cref="ISubscriber.GatewayCustomerId"/> property.
|
||||
/// </summary>
|
||||
|
@ -1,14 +1,31 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Caches;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Braintree;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Stripe;
|
||||
|
||||
using static Bit.Core.Billing.Utilities;
|
||||
using Customer = Stripe.Customer;
|
||||
using Subscription = Stripe.Subscription;
|
||||
|
||||
namespace Bit.Core.Billing.Services.Implementations;
|
||||
|
||||
public class OrganizationBillingService(
|
||||
IBraintreeGateway braintreeGateway,
|
||||
IGlobalSettings globalSettings,
|
||||
ILogger<OrganizationBillingService> logger,
|
||||
IOrganizationRepository organizationRepository,
|
||||
ISetupIntentCache setupIntentCache,
|
||||
IStripeAdapter stripeAdapter,
|
||||
ISubscriberService subscriberService) : IOrganizationBillingService
|
||||
{
|
||||
public async Task<OrganizationMetadata> GetMetadata(Guid organizationId)
|
||||
@ -37,6 +54,310 @@ public class OrganizationBillingService(
|
||||
return new OrganizationMetadata(isOnSecretsManagerStandalone);
|
||||
}
|
||||
|
||||
public async Task PurchaseSubscription(
|
||||
Organization organization,
|
||||
OrganizationSubscriptionPurchase organizationSubscriptionPurchase)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(organization);
|
||||
ArgumentNullException.ThrowIfNull(organizationSubscriptionPurchase);
|
||||
|
||||
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.GatewayCustomerId = customer.Id;
|
||||
organization.GatewaySubscriptionId = subscription.Id;
|
||||
|
||||
await organizationRepository.ReplaceAsync(organization);
|
||||
}
|
||||
|
||||
#region Utilities
|
||||
|
||||
private async Task<Customer> CreateCustomerAsync(
|
||||
Organization organization,
|
||||
OrganizationSubscriptionPurchaseMetadata metadata,
|
||||
TokenizedPaymentSource paymentSource,
|
||||
TaxInformation taxInformation)
|
||||
{
|
||||
if (paymentSource == null)
|
||||
{
|
||||
logger.LogError(
|
||||
"Cannot create customer for organization ({OrganizationID}) without a payment source",
|
||||
organization.Id);
|
||||
|
||||
throw new BillingException();
|
||||
}
|
||||
|
||||
if (taxInformation is not { Country: not null, PostalCode: not null })
|
||||
{
|
||||
logger.LogError(
|
||||
"Cannot create customer for organization ({OrganizationID}) without both a country and postal code",
|
||||
organization.Id);
|
||||
|
||||
throw new BillingException();
|
||||
}
|
||||
|
||||
var (
|
||||
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 customerCreateOptions = new CustomerCreateOptions
|
||||
{
|
||||
Address = address,
|
||||
Coupon = coupon,
|
||||
Description = organization.DisplayBusinessName(),
|
||||
Email = organization.BillingEmail,
|
||||
Expand = ["tax"],
|
||||
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
||||
{
|
||||
CustomFields = [
|
||||
new CustomerInvoiceSettingsCustomFieldOptions
|
||||
{
|
||||
Name = organization.SubscriberType(),
|
||||
Value = organizationDisplayName.Length <= 30
|
||||
? organizationDisplayName
|
||||
: organizationDisplayName[..30]
|
||||
}]
|
||||
},
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
{ "region", globalSettings.BaseServiceUri.CloudRegion }
|
||||
},
|
||||
Tax = new CustomerTaxOptions
|
||||
{
|
||||
ValidateLocation = StripeConstants.ValidateTaxLocationTiming.Immediately
|
||||
},
|
||||
TaxIdData = !string.IsNullOrEmpty(taxId)
|
||||
? [new CustomerTaxIdDataOptions { Type = taxInformation.GetTaxIdType(), Value = taxId }]
|
||||
: null
|
||||
};
|
||||
|
||||
var (type, token) = paymentSource;
|
||||
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
logger.LogError(
|
||||
"Cannot create customer for organization ({OrganizationID}) without a payment source token",
|
||||
organization.Id);
|
||||
|
||||
throw new BillingException();
|
||||
}
|
||||
|
||||
var braintreeCustomerId = "";
|
||||
|
||||
// ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault
|
||||
switch (type)
|
||||
{
|
||||
case PaymentMethodType.BankAccount:
|
||||
{
|
||||
var setupIntent =
|
||||
(await stripeAdapter.SetupIntentList(new SetupIntentListOptions { PaymentMethod = token }))
|
||||
.FirstOrDefault();
|
||||
|
||||
if (setupIntent == null)
|
||||
{
|
||||
logger.LogError("Cannot create customer for organization ({OrganizationID}) without a setup intent for their bank account", organization.Id);
|
||||
|
||||
throw new BillingException();
|
||||
}
|
||||
|
||||
await setupIntentCache.Set(organization.Id, setupIntent.Id);
|
||||
|
||||
break;
|
||||
}
|
||||
case PaymentMethodType.Card:
|
||||
{
|
||||
customerCreateOptions.PaymentMethod = token;
|
||||
customerCreateOptions.InvoiceSettings.DefaultPaymentMethod = token;
|
||||
break;
|
||||
}
|
||||
case PaymentMethodType.PayPal:
|
||||
{
|
||||
braintreeCustomerId = await subscriberService.CreateBraintreeCustomer(organization, token);
|
||||
|
||||
customerCreateOptions.Metadata[BraintreeCustomerIdKey] = braintreeCustomerId;
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
logger.LogError("Cannot create customer for organization ({OrganizationID}) using payment method type ({PaymentMethodType}) as it is not supported", organization.Id, type.ToString());
|
||||
|
||||
throw new BillingException();
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return await stripeAdapter.CustomerCreateAsync(customerCreateOptions);
|
||||
}
|
||||
catch (StripeException stripeException) when (stripeException.StripeError?.Code ==
|
||||
StripeConstants.ErrorCodes.CustomerTaxLocationInvalid)
|
||||
{
|
||||
await Revert();
|
||||
|
||||
throw new BadRequestException(
|
||||
"Your location wasn't recognized. Please ensure your country and postal code are valid.");
|
||||
}
|
||||
catch (StripeException stripeException) when (stripeException.StripeError?.Code ==
|
||||
StripeConstants.ErrorCodes.TaxIdInvalid)
|
||||
{
|
||||
await Revert();
|
||||
|
||||
throw new BadRequestException(
|
||||
"Your tax ID wasn't recognized for your selected country. Please ensure your country and tax ID are valid.");
|
||||
}
|
||||
catch
|
||||
{
|
||||
await Revert();
|
||||
throw;
|
||||
}
|
||||
|
||||
async Task Revert()
|
||||
{
|
||||
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
|
||||
switch (type)
|
||||
{
|
||||
case PaymentMethodType.BankAccount:
|
||||
{
|
||||
await setupIntentCache.Remove(organization.Id);
|
||||
break;
|
||||
}
|
||||
case PaymentMethodType.PayPal:
|
||||
{
|
||||
await braintreeGateway.Customer.DeleteAsync(braintreeCustomerId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Subscription> CreateSubscriptionAsync(
|
||||
Customer customer,
|
||||
Guid organizationId,
|
||||
OrganizationPasswordManagerSubscriptionPurchase passwordManager,
|
||||
PlanType planType,
|
||||
OrganizationSecretsManagerSubscriptionPurchase secretsManager)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(planType);
|
||||
|
||||
if (passwordManager == null)
|
||||
{
|
||||
logger.LogError("Cannot create subscription for organization ({OrganizationID}) without password manager purchase information", organizationId);
|
||||
|
||||
throw new BillingException();
|
||||
}
|
||||
|
||||
var subscriptionItemOptionsList = new List<SubscriptionItemOptions>
|
||||
{
|
||||
new ()
|
||||
{
|
||||
Price = plan.PasswordManager.StripeSeatPlanId,
|
||||
Quantity = passwordManager.Seats
|
||||
}
|
||||
};
|
||||
|
||||
if (passwordManager.PremiumAccess)
|
||||
{
|
||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Price = plan.PasswordManager.StripePremiumAccessPlanId,
|
||||
Quantity = 1
|
||||
});
|
||||
}
|
||||
|
||||
if (passwordManager.Storage > 0)
|
||||
{
|
||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Price = plan.PasswordManager.StripeStoragePlanId,
|
||||
Quantity = passwordManager.Storage
|
||||
});
|
||||
}
|
||||
|
||||
if (secretsManager != null)
|
||||
{
|
||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Price = plan.SecretsManager.StripeSeatPlanId,
|
||||
Quantity = secretsManager.Seats
|
||||
});
|
||||
|
||||
if (secretsManager.ServiceAccounts > 0)
|
||||
{
|
||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Price = plan.SecretsManager.StripeServiceAccountPlanId,
|
||||
Quantity = secretsManager.ServiceAccounts
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var subscriptionCreateOptions = new SubscriptionCreateOptions
|
||||
{
|
||||
AutomaticTax = new SubscriptionAutomaticTaxOptions
|
||||
{
|
||||
Enabled = customer.Tax?.AutomaticTax == StripeConstants.AutomaticTaxStatus.Supported
|
||||
},
|
||||
CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically,
|
||||
Customer = customer.Id,
|
||||
Items = subscriptionItemOptionsList,
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
["organizationId"] = organizationId.ToString()
|
||||
},
|
||||
OffSession = true,
|
||||
TrialPeriodDays = plan.TrialPeriodDays,
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
return await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await stripeAdapter.CustomerDeleteAsync(customer.Id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsOnSecretsManagerStandalone(
|
||||
Organization organization,
|
||||
Customer customer,
|
||||
@ -62,4 +383,6 @@ public class OrganizationBillingService(
|
||||
|
||||
return subscriptionProductIds.Intersect(couponAppliesTo ?? []).Any();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
@ -102,6 +102,37 @@ public class SubscriberService(
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> CreateBraintreeCustomer(
|
||||
ISubscriber subscriber,
|
||||
string paymentMethodNonce)
|
||||
{
|
||||
var braintreeCustomerId =
|
||||
subscriber.BraintreeCustomerIdPrefix() +
|
||||
subscriber.Id.ToString("N").ToLower() +
|
||||
CoreHelpers.RandomString(3, upper: false, numeric: false);
|
||||
|
||||
var customerResult = await braintreeGateway.Customer.CreateAsync(new CustomerRequest
|
||||
{
|
||||
Id = braintreeCustomerId,
|
||||
CustomFields = new Dictionary<string, string>
|
||||
{
|
||||
[subscriber.BraintreeIdField()] = subscriber.Id.ToString(),
|
||||
[subscriber.BraintreeCloudRegionField()] = globalSettings.BaseServiceUri.CloudRegion
|
||||
},
|
||||
Email = subscriber.BillingEmailAddress(),
|
||||
PaymentMethodNonce = paymentMethodNonce,
|
||||
});
|
||||
|
||||
if (customerResult.IsSuccess())
|
||||
{
|
||||
return customerResult.Target.Id;
|
||||
}
|
||||
|
||||
logger.LogError("Failed to create Braintree customer for subscriber ({ID})", subscriber.Id);
|
||||
|
||||
throw new BillingException();
|
||||
}
|
||||
|
||||
public async Task<Customer> GetCustomer(
|
||||
ISubscriber subscriber,
|
||||
CustomerGetOptions customerGetOptions = null)
|
||||
@ -530,7 +561,7 @@ public class SubscriberService(
|
||||
}
|
||||
}
|
||||
|
||||
braintreeCustomerId = await CreateBraintreeCustomerAsync(subscriber, token);
|
||||
braintreeCustomerId = await CreateBraintreeCustomer(subscriber, token);
|
||||
|
||||
await AddBraintreeCustomerIdAsync(customer, braintreeCustomerId);
|
||||
|
||||
@ -648,37 +679,6 @@ public class SubscriberService(
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<string> CreateBraintreeCustomerAsync(
|
||||
ISubscriber subscriber,
|
||||
string paymentMethodNonce)
|
||||
{
|
||||
var braintreeCustomerId =
|
||||
subscriber.BraintreeCustomerIdPrefix() +
|
||||
subscriber.Id.ToString("N").ToLower() +
|
||||
CoreHelpers.RandomString(3, upper: false, numeric: false);
|
||||
|
||||
var customerResult = await braintreeGateway.Customer.CreateAsync(new CustomerRequest
|
||||
{
|
||||
Id = braintreeCustomerId,
|
||||
CustomFields = new Dictionary<string, string>
|
||||
{
|
||||
[subscriber.BraintreeIdField()] = subscriber.Id.ToString(),
|
||||
[subscriber.BraintreeCloudRegionField()] = globalSettings.BaseServiceUri.CloudRegion
|
||||
},
|
||||
Email = subscriber.BillingEmailAddress(),
|
||||
PaymentMethodNonce = paymentMethodNonce,
|
||||
});
|
||||
|
||||
if (customerResult.IsSuccess())
|
||||
{
|
||||
return customerResult.Target.Id;
|
||||
}
|
||||
|
||||
logger.LogError("Failed to create Braintree customer for subscriber ({ID})", subscriber.Id);
|
||||
|
||||
throw new BillingException();
|
||||
}
|
||||
|
||||
private async Task<PaymentSource> GetPaymentSourceAsync(
|
||||
Guid subscriberId,
|
||||
Customer customer)
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Models.Business;
|
||||
@ -14,4 +15,42 @@ public class OrganizationSignup : OrganizationUpgrade
|
||||
public string PaymentToken { get; set; }
|
||||
public int? MaxAutoscaleSeats { get; set; } = null;
|
||||
public string InitiationPath { get; set; }
|
||||
|
||||
public OrganizationSubscriptionPurchase ToSubscriptionPurchase(bool fromProvider = false)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ using System.IdentityModel.Tokens.Jwt;
|
||||
using AspNetCoreRateLimit;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.SecretsManager.Repositories.Noop;
|
||||
@ -145,6 +146,7 @@ public class Startup
|
||||
services.AddBaseServices(globalSettings);
|
||||
services.AddDefaultServices(globalSettings);
|
||||
services.AddCoreLocalizationServices();
|
||||
services.AddBillingOperations();
|
||||
|
||||
// TODO: Remove when OrganizationUser methods are moved out of OrganizationService, this noop dependency should
|
||||
// TODO: no longer be required - see PM-1880
|
||||
|
Loading…
Reference in New Issue
Block a user