1
0
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:
Alex Morask 2024-09-25 08:55:45 -04:00 committed by GitHub
parent 6514b342fc
commit 2e072aebe3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 112 additions and 101 deletions

View File

@ -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;
}

View File

@ -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)

View File

@ -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.

View File

@ -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);