mirror of
https://github.com/bitwarden/server.git
synced 2025-04-06 21:48:12 -05:00
[PM-8445] Allow for organization sales with no payment method for trials (#4800)
* Allow for OrganizationSales with no payment method * Run dotnet format
This commit is contained in:
parent
6514b342fc
commit
2e072aebe3
@ -4,7 +4,9 @@
|
|||||||
|
|
||||||
public class CustomerSetup
|
public class CustomerSetup
|
||||||
{
|
{
|
||||||
public required TokenizedPaymentSource TokenizedPaymentSource { get; set; }
|
public TokenizedPaymentSource? TokenizedPaymentSource { get; set; }
|
||||||
public required TaxInformation TaxInformation { get; set; }
|
public TaxInformation? TaxInformation { get; set; }
|
||||||
public string? Coupon { get; set; }
|
public string? Coupon { get; set; }
|
||||||
|
|
||||||
|
public bool IsBillable => TokenizedPaymentSource != null && TaxInformation != null;
|
||||||
}
|
}
|
||||||
|
@ -41,18 +41,27 @@ public class OrganizationSale
|
|||||||
SubscriptionSetup = GetSubscriptionSetup(upgrade)
|
SubscriptionSetup = GetSubscriptionSetup(upgrade)
|
||||||
};
|
};
|
||||||
|
|
||||||
private static CustomerSetup? GetCustomerSetup(OrganizationSignup signup)
|
private static CustomerSetup GetCustomerSetup(OrganizationSignup signup)
|
||||||
{
|
{
|
||||||
|
var customerSetup = new CustomerSetup
|
||||||
|
{
|
||||||
|
Coupon = signup.IsFromProvider
|
||||||
|
? StripeConstants.CouponIDs.MSPDiscount35
|
||||||
|
: signup.IsFromSecretsManagerTrial
|
||||||
|
? StripeConstants.CouponIDs.SecretsManagerStandalone
|
||||||
|
: null
|
||||||
|
};
|
||||||
|
|
||||||
if (!signup.PaymentMethodType.HasValue)
|
if (!signup.PaymentMethodType.HasValue)
|
||||||
{
|
{
|
||||||
return null;
|
return customerSetup;
|
||||||
}
|
}
|
||||||
|
|
||||||
var tokenizedPaymentSource = new TokenizedPaymentSource(
|
customerSetup.TokenizedPaymentSource = new TokenizedPaymentSource(
|
||||||
signup.PaymentMethodType!.Value,
|
signup.PaymentMethodType!.Value,
|
||||||
signup.PaymentToken);
|
signup.PaymentToken);
|
||||||
|
|
||||||
var taxInformation = new TaxInformation(
|
customerSetup.TaxInformation = new TaxInformation(
|
||||||
signup.TaxInfo.BillingAddressCountry,
|
signup.TaxInfo.BillingAddressCountry,
|
||||||
signup.TaxInfo.BillingAddressPostalCode,
|
signup.TaxInfo.BillingAddressPostalCode,
|
||||||
signup.TaxInfo.TaxIdNumber,
|
signup.TaxInfo.TaxIdNumber,
|
||||||
@ -61,18 +70,7 @@ public class OrganizationSale
|
|||||||
signup.TaxInfo.BillingAddressCity,
|
signup.TaxInfo.BillingAddressCity,
|
||||||
signup.TaxInfo.BillingAddressState);
|
signup.TaxInfo.BillingAddressState);
|
||||||
|
|
||||||
var coupon = signup.IsFromProvider
|
return customerSetup;
|
||||||
? StripeConstants.CouponIDs.MSPDiscount35
|
|
||||||
: signup.IsFromSecretsManagerTrial
|
|
||||||
? StripeConstants.CouponIDs.SecretsManagerStandalone
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return new CustomerSetup
|
|
||||||
{
|
|
||||||
TokenizedPaymentSource = tokenizedPaymentSource,
|
|
||||||
TaxInformation = taxInformation,
|
|
||||||
Coupon = coupon
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SubscriptionSetup GetSubscriptionSetup(OrganizationUpgrade upgrade)
|
private static SubscriptionSetup GetSubscriptionSetup(OrganizationUpgrade upgrade)
|
||||||
|
@ -4,6 +4,8 @@ using Bit.Core.Billing.Models.Sales;
|
|||||||
|
|
||||||
namespace Bit.Core.Billing.Services;
|
namespace Bit.Core.Billing.Services;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
public interface IOrganizationBillingService
|
public interface IOrganizationBillingService
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -29,7 +31,7 @@ public interface IOrganizationBillingService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="organizationId">The ID of the organization to retrieve metadata for.</param>
|
/// <param name="organizationId">The ID of the organization to retrieve metadata for.</param>
|
||||||
/// <returns>An <see cref="OrganizationMetadata"/> record.</returns>
|
/// <returns>An <see cref="OrganizationMetadata"/> record.</returns>
|
||||||
Task<OrganizationMetadata> GetMetadata(Guid organizationId);
|
Task<OrganizationMetadata?> GetMetadata(Guid organizationId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates the provided <paramref name="organization"/>'s payment source and tax information.
|
/// Updates the provided <paramref name="organization"/>'s payment source and tax information.
|
||||||
|
@ -19,6 +19,8 @@ using Subscription = Stripe.Subscription;
|
|||||||
|
|
||||||
namespace Bit.Core.Billing.Services.Implementations;
|
namespace Bit.Core.Billing.Services.Implementations;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
public class OrganizationBillingService(
|
public class OrganizationBillingService(
|
||||||
IBraintreeGateway braintreeGateway,
|
IBraintreeGateway braintreeGateway,
|
||||||
IGlobalSettings globalSettings,
|
IGlobalSettings globalSettings,
|
||||||
@ -53,7 +55,7 @@ public class OrganizationBillingService(
|
|||||||
await organizationRepository.ReplaceAsync(organization);
|
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);
|
||||||
|
|
||||||
@ -90,7 +92,7 @@ public class OrganizationBillingService(
|
|||||||
new CustomerSetup
|
new CustomerSetup
|
||||||
{
|
{
|
||||||
TokenizedPaymentSource = tokenizedPaymentSource,
|
TokenizedPaymentSource = tokenizedPaymentSource,
|
||||||
TaxInformation = taxInformation,
|
TaxInformation = taxInformation
|
||||||
});
|
});
|
||||||
|
|
||||||
organization.Gateway = GatewayType.Stripe;
|
organization.Gateway = GatewayType.Stripe;
|
||||||
@ -110,37 +112,12 @@ public class OrganizationBillingService(
|
|||||||
private async Task<Customer> CreateCustomerAsync(
|
private async Task<Customer> CreateCustomerAsync(
|
||||||
Organization organization,
|
Organization organization,
|
||||||
CustomerSetup customerSetup,
|
CustomerSetup customerSetup,
|
||||||
List<string> expand = null)
|
List<string>? expand = null)
|
||||||
{
|
{
|
||||||
if (customerSetup.TokenizedPaymentSource is not
|
|
||||||
{
|
|
||||||
Type: PaymentMethodType.BankAccount or PaymentMethodType.Card or PaymentMethodType.PayPal,
|
|
||||||
Token: not null and not ""
|
|
||||||
})
|
|
||||||
{
|
|
||||||
logger.LogError(
|
|
||||||
"Cannot create customer for organization ({OrganizationID}) without a valid payment source",
|
|
||||||
organization.Id);
|
|
||||||
|
|
||||||
throw new BillingException();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (customerSetup.TaxInformation is not { Country: not null and not "", PostalCode: not null and not "" })
|
|
||||||
{
|
|
||||||
logger.LogError(
|
|
||||||
"Cannot create customer for organization ({OrganizationID}) without valid tax information",
|
|
||||||
organization.Id);
|
|
||||||
|
|
||||||
throw new BillingException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var (address, taxIdData) = customerSetup.TaxInformation.GetStripeOptions();
|
|
||||||
|
|
||||||
var organizationDisplayName = organization.DisplayName();
|
var organizationDisplayName = organization.DisplayName();
|
||||||
|
|
||||||
var customerCreateOptions = new CustomerCreateOptions
|
var customerCreateOptions = new CustomerCreateOptions
|
||||||
{
|
{
|
||||||
Address = address,
|
|
||||||
Coupon = customerSetup.Coupon,
|
Coupon = customerSetup.Coupon,
|
||||||
Description = organization.DisplayBusinessName(),
|
Description = organization.DisplayBusinessName(),
|
||||||
Email = organization.BillingEmail,
|
Email = organization.BillingEmail,
|
||||||
@ -159,58 +136,87 @@ public class OrganizationBillingService(
|
|||||||
Metadata = new Dictionary<string, string>
|
Metadata = new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
{ "region", globalSettings.BaseServiceUri.CloudRegion }
|
{ "region", globalSettings.BaseServiceUri.CloudRegion }
|
||||||
},
|
}
|
||||||
Tax = new CustomerTaxOptions
|
|
||||||
{
|
|
||||||
ValidateLocation = StripeConstants.ValidateTaxLocationTiming.Immediately
|
|
||||||
},
|
|
||||||
TaxIdData = taxIdData
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var (type, token) = customerSetup.TokenizedPaymentSource;
|
|
||||||
|
|
||||||
var braintreeCustomerId = "";
|
var braintreeCustomerId = "";
|
||||||
|
|
||||||
// ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault
|
if (customerSetup.IsBillable)
|
||||||
switch (type)
|
|
||||||
{
|
{
|
||||||
case PaymentMethodType.BankAccount:
|
if (customerSetup.TokenizedPaymentSource is not
|
||||||
{
|
{
|
||||||
var setupIntent =
|
Type: PaymentMethodType.BankAccount or PaymentMethodType.Card or PaymentMethodType.PayPal,
|
||||||
(await stripeAdapter.SetupIntentList(new SetupIntentListOptions { PaymentMethod = token }))
|
Token: not null and not ""
|
||||||
.FirstOrDefault();
|
})
|
||||||
|
{
|
||||||
|
logger.LogError(
|
||||||
|
"Cannot create customer for organization ({OrganizationID}) without a valid payment source",
|
||||||
|
organization.Id);
|
||||||
|
|
||||||
if (setupIntent == null)
|
throw new BillingException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (customerSetup.TaxInformation is not { Country: not null and not "", PostalCode: not null and not "" })
|
||||||
|
{
|
||||||
|
logger.LogError(
|
||||||
|
"Cannot create customer for organization ({OrganizationID}) without valid tax information",
|
||||||
|
organization.Id);
|
||||||
|
|
||||||
|
throw new BillingException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var (address, taxIdData) = customerSetup.TaxInformation.GetStripeOptions();
|
||||||
|
|
||||||
|
customerCreateOptions.Address = address;
|
||||||
|
customerCreateOptions.Tax = new CustomerTaxOptions
|
||||||
|
{
|
||||||
|
ValidateLocation = StripeConstants.ValidateTaxLocationTiming.Immediately
|
||||||
|
};
|
||||||
|
customerCreateOptions.TaxIdData = taxIdData;
|
||||||
|
|
||||||
|
var (type, token) = customerSetup.TokenizedPaymentSource;
|
||||||
|
|
||||||
|
// ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case PaymentMethodType.BankAccount:
|
||||||
{
|
{
|
||||||
logger.LogError("Cannot create customer for organization ({OrganizationID}) without a setup intent for their bank account", organization.Id);
|
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();
|
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
|
try
|
||||||
@ -241,19 +247,22 @@ public class OrganizationBillingService(
|
|||||||
|
|
||||||
async Task Revert()
|
async Task Revert()
|
||||||
{
|
{
|
||||||
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
|
if (customerSetup.IsBillable)
|
||||||
switch (type)
|
|
||||||
{
|
{
|
||||||
case PaymentMethodType.BankAccount:
|
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
|
||||||
{
|
switch (customerSetup.TokenizedPaymentSource!.Type)
|
||||||
await setupIntentCache.Remove(organization.Id);
|
{
|
||||||
break;
|
case PaymentMethodType.BankAccount:
|
||||||
}
|
{
|
||||||
case PaymentMethodType.PayPal:
|
await setupIntentCache.Remove(organization.Id);
|
||||||
{
|
break;
|
||||||
await braintreeGateway.Customer.DeleteAsync(braintreeCustomerId);
|
}
|
||||||
break;
|
case PaymentMethodType.PayPal:
|
||||||
}
|
{
|
||||||
|
await braintreeGateway.Customer.DeleteAsync(braintreeCustomerId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -334,7 +343,7 @@ public class OrganizationBillingService(
|
|||||||
["organizationId"] = organizationId.ToString()
|
["organizationId"] = organizationId.ToString()
|
||||||
},
|
},
|
||||||
OffSession = true,
|
OffSession = true,
|
||||||
TrialPeriodDays = plan.TrialPeriodDays,
|
TrialPeriodDays = plan.TrialPeriodDays
|
||||||
};
|
};
|
||||||
|
|
||||||
return await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
|
return await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user