mirror of
https://github.com/bitwarden/server.git
synced 2024-11-21 12:05:42 +01:00
[PM-8445] Update trial initiation UI (#4712)
* Add the feature flag Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * Initial comment Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * changes to subscribe with payment method Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * Add new objects * Implementation for subscription without payment method Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * Remove unused codes and classes Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * Rename the flag properly Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * remove implementation that is no longer needed Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * revert the changes on some code removal Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * Resolve the pr comment Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * format the data annotations line breaks Signed-off-by: Cy Okeke <cokeke@bitwarden.com> --------- Signed-off-by: Cy Okeke <cokeke@bitwarden.com>
This commit is contained in:
parent
8c8956da37
commit
c66879eb89
@ -171,6 +171,21 @@ public class OrganizationsController : Controller
|
|||||||
return new OrganizationResponseModel(result.Item1);
|
return new OrganizationResponseModel(result.Item1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("create-without-payment")]
|
||||||
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
|
public async Task<OrganizationResponseModel> CreateWithoutPaymentAsync([FromBody] OrganizationNoPaymentCreateRequest model)
|
||||||
|
{
|
||||||
|
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var organizationSignup = model.ToOrganizationSignup(user);
|
||||||
|
var result = await _organizationService.SignUpAsync(organizationSignup);
|
||||||
|
return new OrganizationResponseModel(result.Item1);
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPut("{id}")]
|
[HttpPut("{id}")]
|
||||||
[HttpPost("{id}")]
|
[HttpPost("{id}")]
|
||||||
public async Task<OrganizationResponseModel> Put(string id, [FromBody] OrganizationUpdateRequestModel model)
|
public async Task<OrganizationResponseModel> Put(string id, [FromBody] OrganizationUpdateRequestModel model)
|
||||||
|
@ -14,42 +14,63 @@ public class OrganizationCreateRequestModel : IValidatableObject
|
|||||||
[StringLength(50, ErrorMessage = "The field Name exceeds the maximum length.")]
|
[StringLength(50, ErrorMessage = "The field Name exceeds the maximum length.")]
|
||||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
|
||||||
[StringLength(50, ErrorMessage = "The field Business Name exceeds the maximum length.")]
|
[StringLength(50, ErrorMessage = "The field Business Name exceeds the maximum length.")]
|
||||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||||
public string BusinessName { get; set; }
|
public string BusinessName { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[StringLength(256)]
|
[StringLength(256)]
|
||||||
[EmailAddress]
|
[EmailAddress]
|
||||||
public string BillingEmail { get; set; }
|
public string BillingEmail { get; set; }
|
||||||
|
|
||||||
public PlanType PlanType { get; set; }
|
public PlanType PlanType { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
public string Key { get; set; }
|
public string Key { get; set; }
|
||||||
|
|
||||||
public OrganizationKeysRequestModel Keys { get; set; }
|
public OrganizationKeysRequestModel Keys { get; set; }
|
||||||
public PaymentMethodType? PaymentMethodType { get; set; }
|
public PaymentMethodType? PaymentMethodType { get; set; }
|
||||||
public string PaymentToken { get; set; }
|
public string PaymentToken { get; set; }
|
||||||
|
|
||||||
[Range(0, int.MaxValue)]
|
[Range(0, int.MaxValue)]
|
||||||
public int AdditionalSeats { get; set; }
|
public int AdditionalSeats { get; set; }
|
||||||
|
|
||||||
[Range(0, 99)]
|
[Range(0, 99)]
|
||||||
public short? AdditionalStorageGb { get; set; }
|
public short? AdditionalStorageGb { get; set; }
|
||||||
|
|
||||||
public bool PremiumAccessAddon { get; set; }
|
public bool PremiumAccessAddon { get; set; }
|
||||||
|
|
||||||
[EncryptedString]
|
[EncryptedString]
|
||||||
[EncryptedStringLength(1000)]
|
[EncryptedStringLength(1000)]
|
||||||
public string CollectionName { get; set; }
|
public string CollectionName { get; set; }
|
||||||
|
|
||||||
public string TaxIdNumber { get; set; }
|
public string TaxIdNumber { get; set; }
|
||||||
|
|
||||||
public string BillingAddressLine1 { get; set; }
|
public string BillingAddressLine1 { get; set; }
|
||||||
|
|
||||||
public string BillingAddressLine2 { get; set; }
|
public string BillingAddressLine2 { get; set; }
|
||||||
|
|
||||||
public string BillingAddressCity { get; set; }
|
public string BillingAddressCity { get; set; }
|
||||||
|
|
||||||
public string BillingAddressState { get; set; }
|
public string BillingAddressState { get; set; }
|
||||||
|
|
||||||
public string BillingAddressPostalCode { get; set; }
|
public string BillingAddressPostalCode { get; set; }
|
||||||
|
|
||||||
[StringLength(2)]
|
[StringLength(2)]
|
||||||
public string BillingAddressCountry { get; set; }
|
public string BillingAddressCountry { get; set; }
|
||||||
|
|
||||||
public int? MaxAutoscaleSeats { get; set; }
|
public int? MaxAutoscaleSeats { get; set; }
|
||||||
|
|
||||||
[Range(0, int.MaxValue)]
|
[Range(0, int.MaxValue)]
|
||||||
public int? AdditionalSmSeats { get; set; }
|
public int? AdditionalSmSeats { get; set; }
|
||||||
|
|
||||||
[Range(0, int.MaxValue)]
|
[Range(0, int.MaxValue)]
|
||||||
public int? AdditionalServiceAccounts { get; set; }
|
public int? AdditionalServiceAccounts { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
public bool UseSecretsManager { get; set; }
|
public bool UseSecretsManager { get; set; }
|
||||||
|
|
||||||
public bool IsFromSecretsManagerTrial { get; set; }
|
public bool IsFromSecretsManagerTrial { get; set; }
|
||||||
|
|
||||||
public string InitiationPath { get; set; }
|
public string InitiationPath { get; set; }
|
||||||
@ -99,16 +120,19 @@ public class OrganizationCreateRequestModel : IValidatableObject
|
|||||||
{
|
{
|
||||||
yield return new ValidationResult("Payment required.", new string[] { nameof(PaymentToken) });
|
yield return new ValidationResult("Payment required.", new string[] { nameof(PaymentToken) });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (PlanType != PlanType.Free && !PaymentMethodType.HasValue)
|
if (PlanType != PlanType.Free && !PaymentMethodType.HasValue)
|
||||||
{
|
{
|
||||||
yield return new ValidationResult("Payment method type required.",
|
yield return new ValidationResult("Payment method type required.",
|
||||||
new string[] { nameof(PaymentMethodType) });
|
new string[] { nameof(PaymentMethodType) });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (PlanType != PlanType.Free && string.IsNullOrWhiteSpace(BillingAddressCountry))
|
if (PlanType != PlanType.Free && string.IsNullOrWhiteSpace(BillingAddressCountry))
|
||||||
{
|
{
|
||||||
yield return new ValidationResult("Country required.",
|
yield return new ValidationResult("Country required.",
|
||||||
new string[] { nameof(BillingAddressCountry) });
|
new string[] { nameof(BillingAddressCountry) });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (PlanType != PlanType.Free && BillingAddressCountry == "US" &&
|
if (PlanType != PlanType.Free && BillingAddressCountry == "US" &&
|
||||||
string.IsNullOrWhiteSpace(BillingAddressPostalCode))
|
string.IsNullOrWhiteSpace(BillingAddressPostalCode))
|
||||||
{
|
{
|
||||||
@ -117,3 +141,4 @@ public class OrganizationCreateRequestModel : IValidatableObject
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,116 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.Business;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
|
namespace Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||||
|
|
||||||
|
public class OrganizationNoPaymentCreateRequest
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
[StringLength(50, ErrorMessage = "The field Name exceeds the maximum length.")]
|
||||||
|
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
[StringLength(50, ErrorMessage = "The field Business Name exceeds the maximum length.")]
|
||||||
|
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||||
|
public string BusinessName { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[StringLength(256)]
|
||||||
|
[EmailAddress]
|
||||||
|
public string BillingEmail { get; set; }
|
||||||
|
|
||||||
|
public PlanType PlanType { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string Key { get; set; }
|
||||||
|
|
||||||
|
public OrganizationKeysRequestModel Keys { get; set; }
|
||||||
|
public PaymentMethodType? PaymentMethodType { get; set; }
|
||||||
|
public string PaymentToken { get; set; }
|
||||||
|
|
||||||
|
[Range(0, int.MaxValue)]
|
||||||
|
public int AdditionalSeats { get; set; }
|
||||||
|
|
||||||
|
[Range(0, 99)]
|
||||||
|
public short? AdditionalStorageGb { get; set; }
|
||||||
|
|
||||||
|
public bool PremiumAccessAddon { get; set; }
|
||||||
|
|
||||||
|
[EncryptedString]
|
||||||
|
[EncryptedStringLength(1000)]
|
||||||
|
public string CollectionName { get; set; }
|
||||||
|
|
||||||
|
public string TaxIdNumber { get; set; }
|
||||||
|
|
||||||
|
public string BillingAddressLine1 { get; set; }
|
||||||
|
|
||||||
|
public string BillingAddressLine2 { get; set; }
|
||||||
|
|
||||||
|
public string BillingAddressCity { get; set; }
|
||||||
|
|
||||||
|
public string BillingAddressState { get; set; }
|
||||||
|
|
||||||
|
public string BillingAddressPostalCode { get; set; }
|
||||||
|
|
||||||
|
[StringLength(2)]
|
||||||
|
public string BillingAddressCountry { get; set; }
|
||||||
|
|
||||||
|
public int? MaxAutoscaleSeats { get; set; }
|
||||||
|
|
||||||
|
[Range(0, int.MaxValue)]
|
||||||
|
public int? AdditionalSmSeats { get; set; }
|
||||||
|
|
||||||
|
[Range(0, int.MaxValue)]
|
||||||
|
public int? AdditionalServiceAccounts { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public bool UseSecretsManager { get; set; }
|
||||||
|
|
||||||
|
public bool IsFromSecretsManagerTrial { get; set; }
|
||||||
|
|
||||||
|
public string InitiationPath { get; set; }
|
||||||
|
|
||||||
|
public virtual OrganizationSignup ToOrganizationSignup(User user)
|
||||||
|
{
|
||||||
|
var orgSignup = new OrganizationSignup
|
||||||
|
{
|
||||||
|
Owner = user,
|
||||||
|
OwnerKey = Key,
|
||||||
|
Name = Name,
|
||||||
|
Plan = PlanType,
|
||||||
|
PaymentMethodType = PaymentMethodType,
|
||||||
|
PaymentToken = PaymentToken,
|
||||||
|
AdditionalSeats = AdditionalSeats,
|
||||||
|
MaxAutoscaleSeats = MaxAutoscaleSeats,
|
||||||
|
AdditionalStorageGb = AdditionalStorageGb.GetValueOrDefault(0),
|
||||||
|
PremiumAccessAddon = PremiumAccessAddon,
|
||||||
|
BillingEmail = BillingEmail,
|
||||||
|
BusinessName = BusinessName,
|
||||||
|
CollectionName = CollectionName,
|
||||||
|
AdditionalSmSeats = AdditionalSmSeats.GetValueOrDefault(),
|
||||||
|
AdditionalServiceAccounts = AdditionalServiceAccounts.GetValueOrDefault(),
|
||||||
|
UseSecretsManager = UseSecretsManager,
|
||||||
|
IsFromSecretsManagerTrial = IsFromSecretsManagerTrial,
|
||||||
|
TaxInfo = new TaxInfo
|
||||||
|
{
|
||||||
|
TaxIdNumber = TaxIdNumber,
|
||||||
|
BillingAddressLine1 = BillingAddressLine1,
|
||||||
|
BillingAddressLine2 = BillingAddressLine2,
|
||||||
|
BillingAddressCity = BillingAddressCity,
|
||||||
|
BillingAddressState = BillingAddressState,
|
||||||
|
BillingAddressPostalCode = BillingAddressPostalCode,
|
||||||
|
BillingAddressCountry = BillingAddressCountry,
|
||||||
|
},
|
||||||
|
InitiationPath = InitiationPath,
|
||||||
|
};
|
||||||
|
|
||||||
|
Keys?.ToOrganizationSignup(orgSignup);
|
||||||
|
|
||||||
|
return orgSignup;
|
||||||
|
}
|
||||||
|
}
|
@ -589,12 +589,22 @@ public class OrganizationService : IOrganizationService
|
|||||||
await _organizationBillingService.Finalize(sale);
|
await _organizationBillingService.Finalize(sale);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
|
if (signup.PaymentMethodType != null)
|
||||||
{
|
{
|
||||||
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, signup.IsFromProvider, signup.AdditionalSmSeats.GetValueOrDefault(),
|
signup.PremiumAccessAddon, signup.TaxInfo, signup.IsFromProvider, signup.AdditionalSmSeats.GetValueOrDefault(),
|
||||||
signup.AdditionalServiceAccounts.GetValueOrDefault(), signup.IsFromSecretsManagerTrial);
|
signup.AdditionalServiceAccounts.GetValueOrDefault(), signup.IsFromSecretsManagerTrial);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _paymentService.PurchaseOrganizationNoPaymentMethod(organization, plan, signup.AdditionalSeats,
|
||||||
|
signup.PremiumAccessAddon, signup.AdditionalSmSeats.GetValueOrDefault(),
|
||||||
|
signup.AdditionalServiceAccounts.GetValueOrDefault(), signup.IsFromSecretsManagerTrial);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var ownerId = signup.IsFromProvider ? default : signup.Owner.Id;
|
var ownerId = signup.IsFromProvider ? default : signup.Owner.Id;
|
||||||
|
@ -142,6 +142,7 @@ public static class FeatureFlagKeys
|
|||||||
public const string CipherKeyEncryption = "cipher-key-encryption";
|
public const string CipherKeyEncryption = "cipher-key-encryption";
|
||||||
public const string EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill";
|
public const string EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill";
|
||||||
public const string StorageReseedRefactor = "storage-reseed-refactor";
|
public const string StorageReseedRefactor = "storage-reseed-refactor";
|
||||||
|
public const string TrialPayment = "PM-8163-trial-payment";
|
||||||
|
|
||||||
public static List<string> GetAllKeys()
|
public static List<string> GetAllKeys()
|
||||||
{
|
{
|
||||||
|
@ -15,6 +15,9 @@ public interface IPaymentService
|
|||||||
string paymentToken, Plan plan, short additionalStorageGb, int additionalSeats,
|
string paymentToken, Plan plan, short additionalStorageGb, int additionalSeats,
|
||||||
bool premiumAccessAddon, TaxInfo taxInfo, bool provider = false, int additionalSmSeats = 0,
|
bool premiumAccessAddon, TaxInfo taxInfo, bool provider = false, int additionalSmSeats = 0,
|
||||||
int additionalServiceAccount = 0, bool signupIsFromSecretsManagerTrial = false);
|
int additionalServiceAccount = 0, bool signupIsFromSecretsManagerTrial = false);
|
||||||
|
Task<string> PurchaseOrganizationNoPaymentMethod(Organization org, Plan plan, int additionalSeats,
|
||||||
|
bool premiumAccessAddon, int additionalSmSeats = 0, int additionalServiceAccount = 0,
|
||||||
|
bool signupIsFromSecretsManagerTrial = false);
|
||||||
Task SponsorOrganizationAsync(Organization org, OrganizationSponsorship sponsorship);
|
Task SponsorOrganizationAsync(Organization org, OrganizationSponsorship sponsorship);
|
||||||
Task RemoveOrganizationSponsorshipAsync(Organization org, OrganizationSponsorship sponsorship);
|
Task RemoveOrganizationSponsorshipAsync(Organization org, OrganizationSponsorship sponsorship);
|
||||||
Task<string> UpgradeFreeOrganizationAsync(Organization org, Plan plan, OrganizationUpgrade upgrade);
|
Task<string> UpgradeFreeOrganizationAsync(Organization org, Plan plan, OrganizationUpgrade upgrade);
|
||||||
|
@ -207,6 +207,77 @@ public class StripePaymentService : IPaymentService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<string> PurchaseOrganizationNoPaymentMethod(Organization org, StaticStore.Plan plan, int additionalSeats, bool premiumAccessAddon,
|
||||||
|
int additionalSmSeats = 0, int additionalServiceAccount = 0, bool signupIsFromSecretsManagerTrial = false)
|
||||||
|
{
|
||||||
|
|
||||||
|
var stripeCustomerMetadata = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "region", _globalSettings.BaseServiceUri.CloudRegion }
|
||||||
|
};
|
||||||
|
var subCreateOptions = new OrganizationPurchaseSubscriptionOptions(org, plan, new TaxInfo(), additionalSeats, 0, premiumAccessAddon
|
||||||
|
, additionalSmSeats, additionalServiceAccount);
|
||||||
|
|
||||||
|
Customer customer = null;
|
||||||
|
Subscription subscription;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var customerCreateOptions = new CustomerCreateOptions
|
||||||
|
{
|
||||||
|
Description = org.DisplayBusinessName(),
|
||||||
|
Email = org.BillingEmail,
|
||||||
|
Metadata = stripeCustomerMetadata,
|
||||||
|
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
||||||
|
{
|
||||||
|
CustomFields =
|
||||||
|
[
|
||||||
|
new CustomerInvoiceSettingsCustomFieldOptions
|
||||||
|
{
|
||||||
|
Name = org.SubscriberType(),
|
||||||
|
Value = GetFirstThirtyCharacters(org.SubscriberName()),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Coupon = signupIsFromSecretsManagerTrial
|
||||||
|
? SecretsManagerStandaloneDiscountId
|
||||||
|
: null,
|
||||||
|
TaxIdData = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
customer = await _stripeAdapter.CustomerCreateAsync(customerCreateOptions);
|
||||||
|
subCreateOptions.AddExpand("latest_invoice.payment_intent");
|
||||||
|
subCreateOptions.Customer = customer.Id;
|
||||||
|
|
||||||
|
subscription = await _stripeAdapter.SubscriptionCreateAsync(subCreateOptions);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error creating customer, walking back operation.");
|
||||||
|
if (customer != null)
|
||||||
|
{
|
||||||
|
await _stripeAdapter.CustomerDeleteAsync(customer.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
org.Gateway = GatewayType.Stripe;
|
||||||
|
org.GatewayCustomerId = customer.Id;
|
||||||
|
org.GatewaySubscriptionId = subscription.Id;
|
||||||
|
|
||||||
|
if (subscription.Status == "incomplete" &&
|
||||||
|
subscription.LatestInvoice?.PaymentIntent?.Status == "requires_action")
|
||||||
|
{
|
||||||
|
org.Enabled = false;
|
||||||
|
return subscription.LatestInvoice.PaymentIntent.ClientSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
org.Enabled = true;
|
||||||
|
org.ExpirationDate = subscription.CurrentPeriodEnd;
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
private async Task ChangeOrganizationSponsorship(
|
private async Task ChangeOrganizationSponsorship(
|
||||||
Organization org,
|
Organization org,
|
||||||
OrganizationSponsorship sponsorship,
|
OrganizationSponsorship sponsorship,
|
||||||
|
Loading…
Reference in New Issue
Block a user