mirror of
https://github.com/bitwarden/server.git
synced 2024-11-24 12:35:25 +01: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 required TokenizedPaymentSource TokenizedPaymentSource { get; set; }
|
||||
public required TaxInformation TaxInformation { get; set; }
|
||||
public TokenizedPaymentSource? TokenizedPaymentSource { get; set; }
|
||||
public TaxInformation? TaxInformation { get; set; }
|
||||
public string? Coupon { get; set; }
|
||||
|
||||
public bool IsBillable => TokenizedPaymentSource != null && TaxInformation != null;
|
||||
}
|
||||
|
@ -41,18 +41,27 @@ public class OrganizationSale
|
||||
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)
|
||||
{
|
||||
return null;
|
||||
return customerSetup;
|
||||
}
|
||||
|
||||
var tokenizedPaymentSource = new TokenizedPaymentSource(
|
||||
customerSetup.TokenizedPaymentSource = new TokenizedPaymentSource(
|
||||
signup.PaymentMethodType!.Value,
|
||||
signup.PaymentToken);
|
||||
|
||||
var taxInformation = new TaxInformation(
|
||||
customerSetup.TaxInformation = new TaxInformation(
|
||||
signup.TaxInfo.BillingAddressCountry,
|
||||
signup.TaxInfo.BillingAddressPostalCode,
|
||||
signup.TaxInfo.TaxIdNumber,
|
||||
@ -61,18 +70,7 @@ public class OrganizationSale
|
||||
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
|
||||
};
|
||||
return customerSetup;
|
||||
}
|
||||
|
||||
private static SubscriptionSetup GetSubscriptionSetup(OrganizationUpgrade upgrade)
|
||||
|
@ -4,6 +4,8 @@ using Bit.Core.Billing.Models.Sales;
|
||||
|
||||
namespace Bit.Core.Billing.Services;
|
||||
|
||||
#nullable enable
|
||||
|
||||
public interface IOrganizationBillingService
|
||||
{
|
||||
/// <summary>
|
||||
@ -29,7 +31,7 @@ public interface IOrganizationBillingService
|
||||
/// </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);
|
||||
Task<OrganizationMetadata?> GetMetadata(Guid organizationId);
|
||||
|
||||
/// <summary>
|
||||
/// 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;
|
||||
|
||||
#nullable enable
|
||||
|
||||
public class OrganizationBillingService(
|
||||
IBraintreeGateway braintreeGateway,
|
||||
IGlobalSettings globalSettings,
|
||||
@ -53,7 +55,7 @@ public class OrganizationBillingService(
|
||||
await organizationRepository.ReplaceAsync(organization);
|
||||
}
|
||||
|
||||
public async Task<OrganizationMetadata> GetMetadata(Guid organizationId)
|
||||
public async Task<OrganizationMetadata?> GetMetadata(Guid organizationId)
|
||||
{
|
||||
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
||||
|
||||
@ -90,7 +92,7 @@ public class OrganizationBillingService(
|
||||
new CustomerSetup
|
||||
{
|
||||
TokenizedPaymentSource = tokenizedPaymentSource,
|
||||
TaxInformation = taxInformation,
|
||||
TaxInformation = taxInformation
|
||||
});
|
||||
|
||||
organization.Gateway = GatewayType.Stripe;
|
||||
@ -110,37 +112,12 @@ public class OrganizationBillingService(
|
||||
private async Task<Customer> CreateCustomerAsync(
|
||||
Organization organization,
|
||||
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 customerCreateOptions = new CustomerCreateOptions
|
||||
{
|
||||
Address = address,
|
||||
Coupon = customerSetup.Coupon,
|
||||
Description = organization.DisplayBusinessName(),
|
||||
Email = organization.BillingEmail,
|
||||
@ -159,58 +136,87 @@ public class OrganizationBillingService(
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
{ "region", globalSettings.BaseServiceUri.CloudRegion }
|
||||
},
|
||||
Tax = new CustomerTaxOptions
|
||||
{
|
||||
ValidateLocation = StripeConstants.ValidateTaxLocationTiming.Immediately
|
||||
},
|
||||
TaxIdData = taxIdData
|
||||
}
|
||||
};
|
||||
|
||||
var (type, token) = customerSetup.TokenizedPaymentSource;
|
||||
|
||||
var braintreeCustomerId = "";
|
||||
|
||||
// ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault
|
||||
switch (type)
|
||||
if (customerSetup.IsBillable)
|
||||
{
|
||||
case PaymentMethodType.BankAccount:
|
||||
if (customerSetup.TokenizedPaymentSource is not
|
||||
{
|
||||
var setupIntent =
|
||||
(await stripeAdapter.SetupIntentList(new SetupIntentListOptions { PaymentMethod = token }))
|
||||
.FirstOrDefault();
|
||||
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);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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
|
||||
@ -241,19 +247,22 @@ public class OrganizationBillingService(
|
||||
|
||||
async Task Revert()
|
||||
{
|
||||
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
|
||||
switch (type)
|
||||
if (customerSetup.IsBillable)
|
||||
{
|
||||
case PaymentMethodType.BankAccount:
|
||||
{
|
||||
await setupIntentCache.Remove(organization.Id);
|
||||
break;
|
||||
}
|
||||
case PaymentMethodType.PayPal:
|
||||
{
|
||||
await braintreeGateway.Customer.DeleteAsync(braintreeCustomerId);
|
||||
break;
|
||||
}
|
||||
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
|
||||
switch (customerSetup.TokenizedPaymentSource!.Type)
|
||||
{
|
||||
case PaymentMethodType.BankAccount:
|
||||
{
|
||||
await setupIntentCache.Remove(organization.Id);
|
||||
break;
|
||||
}
|
||||
case PaymentMethodType.PayPal:
|
||||
{
|
||||
await braintreeGateway.Customer.DeleteAsync(braintreeCustomerId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -334,7 +343,7 @@ public class OrganizationBillingService(
|
||||
["organizationId"] = organizationId.ToString()
|
||||
},
|
||||
OffSession = true,
|
||||
TrialPeriodDays = plan.TrialPeriodDays,
|
||||
TrialPeriodDays = plan.TrialPeriodDays
|
||||
};
|
||||
|
||||
return await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
|
||||
|
Loading…
Reference in New Issue
Block a user