using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Commands;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Microsoft.Extensions.Logging;
using OneOf.Types;
using Stripe;
namespace Bit.Core.Billing.Premium.Commands;
///
/// Upgrades a user's Premium subscription to an Organization plan by creating a new Organization
/// and transferring the subscription from the User to the Organization.
///
public interface IUpgradePremiumToOrganizationCommand
{
///
/// Upgrades a Premium subscription to an Organization subscription.
///
/// The user with an active Premium subscription to upgrade.
/// The name for the new organization.
/// The encrypted organization key for the owner.
/// The target organization plan type to upgrade to.
/// The billing address for tax calculation.
/// A billing command result indicating success or failure with appropriate error details.
Task> Run(
User user,
string organizationName,
string key,
PlanType targetPlanType,
Payment.Models.BillingAddress billingAddress);
}
public class UpgradePremiumToOrganizationCommand(
ILogger logger,
IPricingClient pricingClient,
IStripeAdapter stripeAdapter,
IUserService userService,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
IOrganizationApiKeyRepository organizationApiKeyRepository,
IApplicationCacheService applicationCacheService)
: BaseBillingCommand(logger), IUpgradePremiumToOrganizationCommand
{
public Task> Run(
User user,
string organizationName,
string key,
PlanType targetPlanType,
Payment.Models.BillingAddress billingAddress) => HandleAsync(async () =>
{
// Validate that the user has an active Premium subscription
if (user is not { Premium: true, GatewaySubscriptionId: not null and not "" })
{
return new BadRequest("User does not have an active Premium subscription.");
}
// Hardcode seats to 1 for upgrade flow
const int seats = 1;
// Fetch the current Premium subscription from Stripe
var currentSubscription = await stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId);
// Fetch all premium plans to find which specific plan the user is on
var premiumPlans = await pricingClient.ListPremiumPlans();
// Find the password manager subscription item (seat, not storage) and match it to a plan
var passwordManagerItem = currentSubscription.Items.Data.FirstOrDefault(i =>
premiumPlans.Any(p => p.Seat.StripePriceId == i.Price.Id));
if (passwordManagerItem == null)
{
return new BadRequest("Premium subscription password manager item not found.");
}
var usersPremiumPlan = premiumPlans.First(p => p.Seat.StripePriceId == passwordManagerItem.Price.Id);
// Get the target organization plan
var targetPlan = await pricingClient.GetPlanOrThrow(targetPlanType);
// Build the list of subscription item updates
var subscriptionItemOptions = new List();
// Delete the storage item if it exists for this user's plan
var storageItem = currentSubscription.Items.Data.FirstOrDefault(i =>
i.Price.Id == usersPremiumPlan.Storage.StripePriceId);
// Capture the previous additional storage quantity for potential revert
var previousAdditionalStorage = storageItem?.Quantity ?? 0;
if (storageItem != null)
{
subscriptionItemOptions.Add(new SubscriptionItemOptions
{
Id = storageItem.Id,
Deleted = true
});
}
// Add new organization subscription items
if (targetPlan.HasNonSeatBasedPasswordManagerPlan())
{
subscriptionItemOptions.Add(new SubscriptionItemOptions
{
Id = passwordManagerItem.Id,
Price = targetPlan.PasswordManager.StripePlanId,
Quantity = 1
});
}
else
{
subscriptionItemOptions.Add(new SubscriptionItemOptions
{
Id = passwordManagerItem.Id,
Price = targetPlan.PasswordManager.StripeSeatPlanId,
Quantity = seats
});
}
// Generate organization ID early to include in metadata
var organizationId = CoreHelpers.GenerateComb();
// Build the subscription update options
var subscriptionUpdateOptions = new SubscriptionUpdateOptions
{
Items = subscriptionItemOptions,
ProrationBehavior = StripeConstants.ProrationBehavior.AlwaysInvoice,
BillingCycleAnchor = SubscriptionBillingCycleAnchor.Unchanged,
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true },
Metadata = new Dictionary
{
[StripeConstants.MetadataKeys.OrganizationId] = organizationId.ToString(),
[StripeConstants.MetadataKeys.PreviousPremiumPriceId] = usersPremiumPlan.Seat.StripePriceId,
[StripeConstants.MetadataKeys.PreviousPeriodEndDate] = currentSubscription.GetCurrentPeriodEnd()?.ToString("O") ?? string.Empty,
[StripeConstants.MetadataKeys.PreviousAdditionalStorage] = previousAdditionalStorage.ToString(),
[StripeConstants.MetadataKeys.PreviousPremiumUserId] = user.Id.ToString(),
[StripeConstants.MetadataKeys.UserId] = string.Empty // Remove userId to unlink subscription from User
}
};
// Create the Organization entity
var organization = new Organization
{
Id = organizationId,
Name = organizationName,
BillingEmail = user.Email,
PlanType = targetPlan.Type,
Seats = seats,
MaxCollections = targetPlan.PasswordManager.MaxCollections,
MaxStorageGb = targetPlan.PasswordManager.BaseStorageGb,
UsePolicies = targetPlan.HasPolicies,
UseSso = targetPlan.HasSso,
UseGroups = targetPlan.HasGroups,
UseEvents = targetPlan.HasEvents,
UseDirectory = targetPlan.HasDirectory,
UseTotp = targetPlan.HasTotp,
Use2fa = targetPlan.Has2fa,
UseApi = targetPlan.HasApi,
UseResetPassword = targetPlan.HasResetPassword,
SelfHost = targetPlan.HasSelfHost,
UsersGetPremium = targetPlan.UsersGetPremium,
UseCustomPermissions = targetPlan.HasCustomPermissions,
UseScim = targetPlan.HasScim,
Plan = targetPlan.Name,
Gateway = GatewayType.Stripe,
Enabled = true,
LicenseKey = CoreHelpers.SecureRandomString(20),
CreationDate = DateTime.UtcNow,
RevisionDate = DateTime.UtcNow,
Status = OrganizationStatusType.Created,
UsePasswordManager = true,
UseSecretsManager = false,
UseOrganizationDomains = targetPlan.HasOrganizationDomains,
GatewayCustomerId = user.GatewayCustomerId,
GatewaySubscriptionId = currentSubscription.Id
};
// Update customer billing address for tax calculation
await stripeAdapter.UpdateCustomerAsync(user.GatewayCustomerId, new CustomerUpdateOptions
{
Address = new AddressOptions
{
Country = billingAddress.Country,
PostalCode = billingAddress.PostalCode
}
});
// Update the subscription in Stripe
await stripeAdapter.UpdateSubscriptionAsync(currentSubscription.Id, subscriptionUpdateOptions);
// Save the organization
await organizationRepository.CreateAsync(organization);
// Create organization API key
await organizationApiKeyRepository.CreateAsync(new OrganizationApiKey
{
OrganizationId = organization.Id,
ApiKey = CoreHelpers.SecureRandomString(30),
Type = OrganizationApiKeyType.Default,
RevisionDate = DateTime.UtcNow,
});
// Update cache
await applicationCacheService.UpsertOrganizationAbilityAsync(organization);
// Create OrganizationUser for the upgrading user as owner
var organizationUser = new OrganizationUser
{
OrganizationId = organization.Id,
UserId = user.Id,
Key = key,
AccessSecretsManager = false,
Type = OrganizationUserType.Owner,
Status = OrganizationUserStatusType.Confirmed,
CreationDate = organization.CreationDate,
RevisionDate = organization.CreationDate
};
organizationUser.SetNewId();
await organizationUserRepository.CreateAsync(organizationUser);
// Remove subscription from user
user.Premium = false;
user.PremiumExpirationDate = null;
user.GatewaySubscriptionId = null;
user.GatewayCustomerId = null;
user.RevisionDate = DateTime.UtcNow;
await userService.SaveUserAsync(user);
return new None();
});
}