Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Billing/pm 11728/wip #4756

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -551,7 +551,7 @@ public async Task<ProviderOrganization> CreateOrganizationAsync(Guid providerId,

var (organization, _, defaultCollection) = consolidatedBillingEnabled
? await _organizationService.SignupClientAsync(organizationSignup)
: await _organizationService.SignUpAsync(organizationSignup, true);
: await _organizationService.SignUpAsync(organizationSignup);

var providerOrganization = new ProviderOrganization
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -667,7 +667,7 @@ public async Task CreateOrganizationAsync_Success(Provider provider, Organizatio

sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
sutProvider.GetDependency<IOrganizationService>().SignUpAsync(organizationSignup, true)
sutProvider.GetDependency<IOrganizationService>().SignUpAsync(organizationSignup)
.Returns((organization, null as OrganizationUser, new Collection()));

var providerOrganization =
Expand Down Expand Up @@ -775,7 +775,7 @@ public async Task CreateOrganizationAsync_SetsAccessAllToFalse

sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
sutProvider.GetDependency<IOrganizationService>().SignUpAsync(organizationSignup, true)
sutProvider.GetDependency<IOrganizationService>().SignUpAsync(organizationSignup)
.Returns((organization, null as OrganizationUser, defaultCollection));

var providerOrganization =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
}

var organizationSignup = model.OrganizationCreateRequest.ToOrganizationSignup(user);
organizationSignup.IsFromProvider = true;

Check warning on line 87 in src/Api/AdminConsole/Controllers/ProviderOrganizationsController.cs

View check run for this annotation

Codecov / codecov/patch

src/Api/AdminConsole/Controllers/ProviderOrganizationsController.cs#L87

Added line #L87 was not covered by tests
var result = await _providerService.CreateOrganizationAsync(providerId, organizationSignup, model.ClientOwnerEmail, user);
return new ProviderOrganizationResponseModel(result);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,11 +182,9 @@

var tokenizedPaymentSource = requestBody.PaymentSource.ToDomain();

await subscriberService.UpdatePaymentSource(organization, tokenizedPaymentSource);

var taxInformation = requestBody.TaxInformation.ToDomain();

await subscriberService.UpdateTaxInformation(organization, taxInformation);
await organizationBillingService.UpdatePaymentMethod(organization, tokenizedPaymentSource, taxInformation);

Check warning on line 187 in src/Api/Billing/Controllers/OrganizationBillingController.cs

View check run for this annotation

Codecov / codecov/patch

src/Api/Billing/Controllers/OrganizationBillingController.cs#L187

Added line #L187 was not covered by tests

return TypedResults.Ok();
}
Expand Down
3 changes: 2 additions & 1 deletion src/Api/Billing/Controllers/ProviderClientsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ public async Task<IResult> CreateAsync(
OwnerKey = requestBody.Key,
PublicKey = requestBody.KeyPair.PublicKey,
PrivateKey = requestBody.KeyPair.EncryptedPrivateKey,
CollectionName = requestBody.CollectionName
CollectionName = requestBody.CollectionName,
IsFromProvider = true
};

var providerOrganization = await providerService.CreateOrganizationAsync(
Expand Down
2 changes: 1 addition & 1 deletion src/Core/AdminConsole/Services/IOrganizationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken, Payment
/// </summary>
/// <returns>A tuple containing the new organization, the initial organizationUser (if any) and the default collection (if any)</returns>
#nullable enable
Task<(Organization organization, OrganizationUser? organizationUser, Collection? defaultCollection)> SignUpAsync(OrganizationSignup organizationSignup, bool provider = false);
Task<(Organization organization, OrganizationUser? organizationUser, Collection? defaultCollection)> SignUpAsync(OrganizationSignup organizationSignup);

Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignupClientAsync(OrganizationSignup signup);
#nullable disable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models.Sales;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
using Bit.Core.Entities;
Expand Down Expand Up @@ -502,24 +503,23 @@
/// <summary>
/// Create a new organization in a cloud environment
/// </summary>
public async Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignUpAsync(OrganizationSignup signup,
bool provider = false)
public async Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignUpAsync(OrganizationSignup signup)
{
var plan = StaticStore.GetPlan(signup.Plan);

ValidatePasswordManagerPlan(plan, signup);

if (signup.UseSecretsManager)
{
if (provider)
if (signup.IsFromProvider)
{
throw new BadRequestException(
"Organizations with a Managed Service Provider do not support Secrets Manager.");
}
ValidateSecretsManagerPlan(plan, signup);
}

if (!provider)
if (!signup.IsFromProvider)
{
await ValidateSignUpPoliciesAsync(signup.Owner.Id);
}
Expand Down Expand Up @@ -570,7 +570,7 @@
signup.AdditionalServiceAccounts.GetValueOrDefault();
}

if (plan.Type == PlanType.Free && !provider)
if (plan.Type == PlanType.Free && !signup.IsFromProvider)
{
var adminCount =
await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(signup.Owner.Id);
Expand All @@ -585,20 +585,19 @@

if (deprecateStripeSourcesAPI)
{
var subscriptionPurchase = signup.ToSubscriptionPurchase(provider);

await _organizationBillingService.PurchaseSubscription(organization, subscriptionPurchase);
var sale = BitwardenOrganizationSale.From(organization, signup);
await _organizationBillingService.Finalize(sale);

Check warning on line 589 in src/Core/AdminConsole/Services/Implementations/OrganizationService.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/Services/Implementations/OrganizationService.cs#L588-L589

Added lines #L588 - L589 were not covered by tests
}
else
{
await _paymentService.PurchaseOrganizationAsync(organization, signup.PaymentMethodType.Value,
signup.PaymentToken, plan, signup.AdditionalStorageGb, signup.AdditionalSeats,
signup.PremiumAccessAddon, signup.TaxInfo, provider, signup.AdditionalSmSeats.GetValueOrDefault(),
signup.PremiumAccessAddon, signup.TaxInfo, signup.IsFromProvider, signup.AdditionalSmSeats.GetValueOrDefault(),
signup.AdditionalServiceAccounts.GetValueOrDefault(), signup.IsFromSecretsManagerTrial);
}
}

var ownerId = provider ? default : signup.Owner.Id;
var ownerId = signup.IsFromProvider ? default : signup.Owner.Id;
var returnValue = await SignUpAsync(organization, ownerId, signup.OwnerKey, signup.CollectionName, true);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.Signup, organization, _currentContext)
Expand Down
27 changes: 0 additions & 27 deletions src/Core/Billing/Models/OrganizationSubscriptionPurchase.cs

This file was deleted.

101 changes: 101 additions & 0 deletions src/Core/Billing/Models/Sales/BitwardenOrganizationSale.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
๏ปฟusing Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Constants;
using Bit.Core.Models.Business;

namespace Bit.Core.Billing.Models.Sales;

#nullable enable

public class BitwardenOrganizationSale
{
private BitwardenOrganizationSale() {}

Check warning on line 11 in src/Core/Billing/Models/Sales/BitwardenOrganizationSale.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/Billing/Models/Sales/BitwardenOrganizationSale.cs#L11

Added line #L11 was not covered by tests

public void Deconstruct(
out Organization organization,
out CustomerSetup? customerSetup,
out SubscriptionSetup subscriptionSetup)
{
organization = Organization;
customerSetup = CustomerSetup;
subscriptionSetup = SubscriptionSetup;
}

Check warning on line 21 in src/Core/Billing/Models/Sales/BitwardenOrganizationSale.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/Billing/Models/Sales/BitwardenOrganizationSale.cs#L17-L21

Added lines #L17 - L21 were not covered by tests

public required Organization Organization { get; set; }
public CustomerSetup? CustomerSetup { get; set; }
public required SubscriptionSetup SubscriptionSetup { get; set; }

Check warning on line 25 in src/Core/Billing/Models/Sales/BitwardenOrganizationSale.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/Billing/Models/Sales/BitwardenOrganizationSale.cs#L23-L25

Added lines #L23 - L25 were not covered by tests

public static BitwardenOrganizationSale From(
Organization organization,
OrganizationSignup signup) => new ()
{
Organization = organization,
CustomerSetup = string.IsNullOrEmpty(organization.GatewayCustomerId) ? GetCustomerSetup(signup) : null,
SubscriptionSetup = GetSubscriptionSetup(signup)
};

Check warning on line 34 in src/Core/Billing/Models/Sales/BitwardenOrganizationSale.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/Billing/Models/Sales/BitwardenOrganizationSale.cs#L30-L34

Added lines #L30 - L34 were not covered by tests

public static BitwardenOrganizationSale From(
Organization organization,
OrganizationUpgrade upgrade) => new()
{
Organization = organization,
SubscriptionSetup = GetSubscriptionSetup(upgrade)
};

Check warning on line 42 in src/Core/Billing/Models/Sales/BitwardenOrganizationSale.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/Billing/Models/Sales/BitwardenOrganizationSale.cs#L38-L42

Added lines #L38 - L42 were not covered by tests

private static CustomerSetup? GetCustomerSetup(OrganizationSignup signup)
{

Check warning on line 45 in src/Core/Billing/Models/Sales/BitwardenOrganizationSale.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/Billing/Models/Sales/BitwardenOrganizationSale.cs#L45

Added line #L45 was not covered by tests
if (!signup.PaymentMethodType.HasValue)
{
return null;

Check warning on line 48 in src/Core/Billing/Models/Sales/BitwardenOrganizationSale.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/Billing/Models/Sales/BitwardenOrganizationSale.cs#L47-L48

Added lines #L47 - L48 were not covered by tests
}

var tokenizedPaymentSource = new TokenizedPaymentSource(
signup.PaymentMethodType!.Value,
signup.PaymentToken);

Check warning on line 53 in src/Core/Billing/Models/Sales/BitwardenOrganizationSale.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/Billing/Models/Sales/BitwardenOrganizationSale.cs#L51-L53

Added lines #L51 - L53 were not covered by tests

var taxInformation = new TaxInformation(
signup.TaxInfo.BillingAddressCountry,
signup.TaxInfo.BillingAddressPostalCode,
signup.TaxInfo.TaxIdNumber,
signup.TaxInfo.BillingAddressLine1,
signup.TaxInfo.BillingAddressLine2,
signup.TaxInfo.BillingAddressCity,
signup.TaxInfo.BillingAddressState);

Check warning on line 62 in src/Core/Billing/Models/Sales/BitwardenOrganizationSale.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/Billing/Models/Sales/BitwardenOrganizationSale.cs#L55-L62

Added lines #L55 - L62 were not covered by tests

var coupon = signup.IsFromProvider
? StripeConstants.CouponIDs.MSPDiscount35
: signup.IsFromSecretsManagerTrial
? StripeConstants.CouponIDs.SecretsManagerStandalone
: null;

Check warning on line 68 in src/Core/Billing/Models/Sales/BitwardenOrganizationSale.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/Billing/Models/Sales/BitwardenOrganizationSale.cs#L65-L68

Added lines #L65 - L68 were not covered by tests

return new CustomerSetup
{
TokenizedPaymentSource = tokenizedPaymentSource, TaxInformation = taxInformation, Coupon = coupon
};
}

Check warning on line 74 in src/Core/Billing/Models/Sales/BitwardenOrganizationSale.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/Billing/Models/Sales/BitwardenOrganizationSale.cs#L70-L74

Added lines #L70 - L74 were not covered by tests

private static SubscriptionSetup GetSubscriptionSetup(OrganizationUpgrade upgrade)
{
var plan = Core.Utilities.StaticStore.GetPlan(upgrade.Plan);

Check warning on line 78 in src/Core/Billing/Models/Sales/BitwardenOrganizationSale.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/Billing/Models/Sales/BitwardenOrganizationSale.cs#L77-L78

Added lines #L77 - L78 were not covered by tests

var passwordManagerOptions = new SubscriptionSetup.PasswordManager
{
Seats = upgrade.AdditionalSeats,
Storage = upgrade.AdditionalStorageGb,
PremiumAccess = upgrade.PremiumAccessAddon
};

Check warning on line 85 in src/Core/Billing/Models/Sales/BitwardenOrganizationSale.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/Billing/Models/Sales/BitwardenOrganizationSale.cs#L80-L85

Added lines #L80 - L85 were not covered by tests

var secretsManagerOptions = upgrade.UseSecretsManager
? new SubscriptionSetup.SecretsManager
{
Seats = upgrade.AdditionalSmSeats ?? 0, ServiceAccounts = upgrade.AdditionalServiceAccounts
}
: null;

Check warning on line 92 in src/Core/Billing/Models/Sales/BitwardenOrganizationSale.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/Billing/Models/Sales/BitwardenOrganizationSale.cs#L88-L92

Added lines #L88 - L92 were not covered by tests

return new SubscriptionSetup
{
Plan = plan,
PasswordManagerOptions = passwordManagerOptions,
SecretsManagerOptions = secretsManagerOptions
};
}

Check warning on line 100 in src/Core/Billing/Models/Sales/BitwardenOrganizationSale.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/Billing/Models/Sales/BitwardenOrganizationSale.cs#L94-L100

Added lines #L94 - L100 were not covered by tests
}
10 changes: 10 additions & 0 deletions src/Core/Billing/Models/Sales/CustomerSetup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
๏ปฟnamespace Bit.Core.Billing.Models.Sales;

#nullable enable

public class CustomerSetup
{
public required TokenizedPaymentSource TokenizedPaymentSource { get; set; }
public required TaxInformation TaxInformation { get; set; }
public string? Coupon { get; set; }

Check warning on line 9 in src/Core/Billing/Models/Sales/CustomerSetup.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/Billing/Models/Sales/CustomerSetup.cs#L7-L9

Added lines #L7 - L9 were not covered by tests
}
25 changes: 25 additions & 0 deletions src/Core/Billing/Models/Sales/SubscriptionSetup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
๏ปฟusing Bit.Core.Models.StaticStore;

namespace Bit.Core.Billing.Models.Sales;

#nullable enable

public class SubscriptionSetup
{
public required Plan Plan { get; set; }
public required PasswordManager PasswordManagerOptions { get; set; }
public SecretsManager? SecretsManagerOptions { get; set; }

Check warning on line 11 in src/Core/Billing/Models/Sales/SubscriptionSetup.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/Billing/Models/Sales/SubscriptionSetup.cs#L9-L11

Added lines #L9 - L11 were not covered by tests

public class PasswordManager
{
public required int Seats { get; set; }
public short? Storage { get; set; }
public bool? PremiumAccess { get; set; }

Check warning on line 17 in src/Core/Billing/Models/Sales/SubscriptionSetup.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/Billing/Models/Sales/SubscriptionSetup.cs#L15-L17

Added lines #L15 - L17 were not covered by tests
}

public class SecretsManager
{
public required int Seats { get; set; }
public int? ServiceAccounts { get; set; }

Check warning on line 23 in src/Core/Billing/Models/Sales/SubscriptionSetup.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/Billing/Models/Sales/SubscriptionSetup.cs#L22-L23

Added lines #L22 - L23 were not covered by tests
}
}
3 changes: 3 additions & 0 deletions src/Core/Billing/Models/StaticStore/Plan.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ public abstract record Plan
public SecretsManagerPlanFeatures SecretsManager { get; protected init; }
public bool SupportsSecretsManager => SecretsManager != null;

public bool HasNonSeatBasedPasswordManagerPlan() =>
PasswordManager is { StripePlanId: not null and not "", StripeSeatPlanId: null or "" };

public record SecretsManagerPlanFeatures
{
// Service accounts
Expand Down
23 changes: 22 additions & 1 deletion src/Core/Billing/Models/TaxInformation.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
๏ปฟnamespace Bit.Core.Billing.Models;
๏ปฟusing Stripe;

namespace Bit.Core.Billing.Models;

public record TaxInformation(
string Country,
Expand All @@ -9,7 +11,26 @@
string City,
string State)
{
public (AddressOptions, List<CustomerTaxIdDataOptions>) GetStripeOptions()
{
var address = new AddressOptions
{
Country = Country,
PostalCode = PostalCode,
Line1 = Line1,
Line2 = Line2,
City = City,
State = State
};

Check warning on line 24 in src/Core/Billing/Models/TaxInformation.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/Billing/Models/TaxInformation.cs#L15-L24

Added lines #L15 - L24 were not covered by tests

var customerTaxIdDataOptionsList = !string.IsNullOrEmpty(TaxId)
? new List<CustomerTaxIdDataOptions> { new() { Type = GetTaxIdType(), Value = TaxId } }
: null;

Check warning on line 28 in src/Core/Billing/Models/TaxInformation.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/Billing/Models/TaxInformation.cs#L27-L28

Added lines #L27 - L28 were not covered by tests

return (address, customerTaxIdDataOptionsList);
}

public string GetTaxIdType()

Check warning on line 33 in src/Core/Billing/Models/TaxInformation.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/Billing/Models/TaxInformation.cs#L30-L33

Added lines #L30 - L33 were not covered by tests
{
if (string.IsNullOrEmpty(Country) || string.IsNullOrEmpty(TaxId))
{
Expand Down
35 changes: 29 additions & 6 deletions src/Core/Billing/Services/IOrganizationBillingService.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,29 @@
๏ปฟusing Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Models.Sales;

namespace Bit.Core.Billing.Services;

public interface IOrganizationBillingService
{
/// <summary>
/// <para>Establishes the billing configuration for a Bitwarden <see cref="Organization"/> using the provided <paramref name="sale"/>.</para>
/// <para>
/// The method first checks to see if the
/// provided <see cref="BitwardenOrganizationSale.Organization"/> already has a Stripe <see cref="Stripe.Customer"/> using the <see cref="Organization.GatewayCustomerId"/>.
/// If it doesn't, the method creates one using the <paramref name="sale"/>'s <see cref="BitwardenOrganizationSale.CustomerSetup"/>. The method then creates a Stripe <see cref="Stripe.Subscription"/>
/// for the created or existing customer using the provided <see cref="BitwardenOrganizationSale.SubscriptionSetup"/>.
/// </para>
/// </summary>
/// <param name="sale">The purchase details necessary to establish the Stripe entities responsible for billing the organization.</param>
/// <example>
/// <code>
/// var sale = BitwardenOrganizationSale.From(organization, organizationSignup);
/// await organizationBillingService.Finalize(sale);
/// </code>
/// </example>
Task Finalize(BitwardenOrganizationSale sale);

/// <summary>
/// Retrieve metadata about the organization represented bsy the provided <paramref name="organizationId"/>.
/// </summary>
Expand All @@ -13,11 +32,15 @@ public interface IOrganizationBillingService
Task<OrganizationMetadata> GetMetadata(Guid organizationId);

/// <summary>
/// Purchase a subscription for the provided <paramref name="organization"/> using the provided <paramref name="organizationSubscriptionPurchase"/>.
/// If successful, a Stripe <see cref="Stripe.Customer"/> and <see cref="Stripe.Subscription"/> will be created for the organization and the
/// organization will be enabled.
/// Updates the provided <paramref name="organization"/>'s payment source and tax information.
/// If the <paramref name="organization"/> does not have a Stripe <see cref="Stripe.Customer"/>, this method will create one using the provided
/// <paramref name="tokenizedPaymentSource"/> and <paramref name="taxInformation"/>.
/// </summary>
/// <param name="organization">The organization to purchase a subscription for.</param>
/// <param name="organizationSubscriptionPurchase">The purchase information for the organization's subscription.</param>
Task PurchaseSubscription(Organization organization, OrganizationSubscriptionPurchase organizationSubscriptionPurchase);
/// <param name="organization">The <paramref name="organization"/> to update the payment source and tax information for.</param>
/// <param name="tokenizedPaymentSource">The tokenized payment source (ex. Credit Card) to attach to the <paramref name="organization"/>.</param>
/// <param name="taxInformation">The <paramref name="organization"/>'s updated tax information.</param>
Task UpdatePaymentMethod(
Organization organization,
TokenizedPaymentSource tokenizedPaymentSource,
TaxInformation taxInformation);
}
Loading
Loading