Skip to content

Commit

Permalink
[PM-8445] Update trial initiation UI (#4712)
Browse files Browse the repository at this point in the history
* Add the feature flag

Signed-off-by: Cy Okeke <[email protected]>

* Initial comment

Signed-off-by: Cy Okeke <[email protected]>

* changes to subscribe with payment method

Signed-off-by: Cy Okeke <[email protected]>

* Add new objects

* Implementation for subscription without payment method

Signed-off-by: Cy Okeke <[email protected]>

* Remove unused codes and classes

Signed-off-by: Cy Okeke <[email protected]>

* Rename the flag properly

Signed-off-by: Cy Okeke <[email protected]>

* remove implementation that is no longer needed

Signed-off-by: Cy Okeke <[email protected]>

* revert the changes on some code removal

Signed-off-by: Cy Okeke <[email protected]>

* Resolve the pr comment

Signed-off-by: Cy Okeke <[email protected]>

* format the data annotations line breaks

Signed-off-by: Cy Okeke <[email protected]>

---------

Signed-off-by: Cy Okeke <[email protected]>
  • Loading branch information
cyprain-okeke authored Sep 27, 2024
1 parent 8c8956d commit c66879e
Show file tree
Hide file tree
Showing 7 changed files with 245 additions and 4 deletions.
15 changes: 15 additions & 0 deletions src/Api/AdminConsole/Controllers/OrganizationsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,21 @@ public async Task<OrganizationResponseModel> Post([FromBody] OrganizationCreateR
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}")]
[HttpPost("{id}")]
public async Task<OrganizationResponseModel> Put(string id, [FromBody] OrganizationUpdateRequestModel model)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,42 +14,63 @@ public class OrganizationCreateRequestModel : IValidatableObject
[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; }
Expand Down Expand Up @@ -99,16 +120,19 @@ public IEnumerable<ValidationResult> Validate(ValidationContext validationContex
{
yield return new ValidationResult("Payment required.", new string[] { nameof(PaymentToken) });
}

if (PlanType != PlanType.Free && !PaymentMethodType.HasValue)
{
yield return new ValidationResult("Payment method type required.",
new string[] { nameof(PaymentMethodType) });
}

if (PlanType != PlanType.Free && string.IsNullOrWhiteSpace(BillingAddressCountry))
{
yield return new ValidationResult("Country required.",
new string[] { nameof(BillingAddressCountry) });
}

if (PlanType != PlanType.Free && BillingAddressCountry == "US" &&
string.IsNullOrWhiteSpace(BillingAddressPostalCode))
{
Expand All @@ -117,3 +141,4 @@ public IEnumerable<ValidationResult> Validate(ValidationContext validationContex
}
}
}

Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -590,10 +590,20 @@ await _referenceEventService.RaiseEventAsync(
}
else
{
await _paymentService.PurchaseOrganizationAsync(organization, signup.PaymentMethodType.Value,
signup.PaymentToken, plan, signup.AdditionalStorageGb, signup.AdditionalSeats,
signup.PremiumAccessAddon, signup.TaxInfo, signup.IsFromProvider, signup.AdditionalSmSeats.GetValueOrDefault(),
signup.AdditionalServiceAccounts.GetValueOrDefault(), signup.IsFromSecretsManagerTrial);
if (signup.PaymentMethodType != null)
{
await _paymentService.PurchaseOrganizationAsync(organization, signup.PaymentMethodType.Value,
signup.PaymentToken, plan, signup.AdditionalStorageGb, signup.AdditionalSeats,
signup.PremiumAccessAddon, signup.TaxInfo, signup.IsFromProvider, signup.AdditionalSmSeats.GetValueOrDefault(),
signup.AdditionalServiceAccounts.GetValueOrDefault(), signup.IsFromSecretsManagerTrial);
}
else
{
await _paymentService.PurchaseOrganizationNoPaymentMethod(organization, plan, signup.AdditionalSeats,
signup.PremiumAccessAddon, signup.AdditionalSmSeats.GetValueOrDefault(),
signup.AdditionalServiceAccounts.GetValueOrDefault(), signup.IsFromSecretsManagerTrial);
}

}
}

Expand Down
1 change: 1 addition & 0 deletions src/Core/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ public static class FeatureFlagKeys
public const string CipherKeyEncryption = "cipher-key-encryption";
public const string EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill";
public const string StorageReseedRefactor = "storage-reseed-refactor";
public const string TrialPayment = "PM-8163-trial-payment";

public static List<string> GetAllKeys()
{
Expand Down
3 changes: 3 additions & 0 deletions src/Core/Services/IPaymentService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ Task<string> PurchaseOrganizationAsync(Organization org, PaymentMethodType payme
string paymentToken, Plan plan, short additionalStorageGb, int additionalSeats,
bool premiumAccessAddon, TaxInfo taxInfo, bool provider = false, int additionalSmSeats = 0,
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 RemoveOrganizationSponsorshipAsync(Organization org, OrganizationSponsorship sponsorship);
Task<string> UpgradeFreeOrganizationAsync(Organization org, Plan plan, OrganizationUpgrade upgrade);
Expand Down
71 changes: 71 additions & 0 deletions src/Core/Services/Implementations/StripePaymentService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,77 @@ public async Task<string> PurchaseOrganizationAsync(Organization org, PaymentMet
}
}

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(
Organization org,
OrganizationSponsorship sponsorship,
Expand Down

0 comments on commit c66879e

Please sign in to comment.