Skip to content
Open
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 @@ -132,8 +132,8 @@ public async Task<IResult> UpgradePremiumToOrganizationAsync(
[BindNever] User user,
[FromBody] UpgradePremiumToOrganizationRequest request)
{
var (organizationName, key, planType, billingAddress) = request.ToDomain();
var result = await upgradePremiumToOrganizationCommand.Run(user, organizationName, key, planType, billingAddress);
var (organizationName, key, publicKey, encryptedPrivateKey, collectionName, planType, billingAddress) = request.ToDomain();
var result = await upgradePremiumToOrganizationCommand.Run(user, organizationName, key, publicKey, encryptedPrivateKey, collectionName, planType, billingAddress);
return Handle(result);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ public class UpgradePremiumToOrganizationRequest
[Required]
public string Key { get; set; } = null!;

[Required]
public string PublicKey { get; set; } = null!;

[Required]
public string EncryptedPrivateKey { get; set; } = null!;

public string? CollectionName { get; set; }

[Required]
[JsonConverter(typeof(JsonStringEnumConverter))]
public required ProductTierType TargetProductTierType { get; set; }
Expand All @@ -39,6 +47,6 @@ private PlanType PlanType
}
}

public (string OrganizationName, string Key, PlanType PlanType, Core.Billing.Payment.Models.BillingAddress BillingAddress) ToDomain() =>
(OrganizationName, Key, PlanType, BillingAddress.ToDomain());
public (string OrganizationName, string Key, string PublicKey, string EncryptedPrivateKey, string? CollectionName, PlanType PlanType, Core.Billing.Payment.Models.BillingAddress BillingAddress) ToDomain() =>
(OrganizationName, Key, PublicKey, EncryptedPrivateKey, CollectionName, PlanType, BillingAddress.ToDomain());
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
using Bit.Core.Billing.Services;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
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;
Expand All @@ -26,13 +26,19 @@ public interface IUpgradePremiumToOrganizationCommand
/// <param name="user">The user with an active Premium subscription to upgrade.</param>
/// <param name="organizationName">The name for the new organization.</param>
/// <param name="key">The encrypted organization key for the owner.</param>
/// <param name="publicKey">The organization's public key.</param>
/// <param name="encryptedPrivateKey">The organization's encrypted private key.</param>
/// <param name="collectionName">Optional name for the default collection.</param>
/// <param name="targetPlanType">The target organization plan type to upgrade to.</param>
/// <param name="billingAddress">The billing address for tax calculation.</param>
/// <returns>A billing command result indicating success or failure with appropriate error details.</returns>
Task<BillingCommandResult<None>> Run(
/// <returns>A billing command result containing the new organization ID on success, or error details on failure.</returns>
Task<BillingCommandResult<Guid>> Run(
User user,
string organizationName,
string key,
string publicKey,
string encryptedPrivateKey,
string? collectionName,
PlanType targetPlanType,
Payment.Models.BillingAddress billingAddress);
}
Expand All @@ -45,15 +51,21 @@ public class UpgradePremiumToOrganizationCommand(
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
IOrganizationApiKeyRepository organizationApiKeyRepository,
ICollectionRepository collectionRepository,
IApplicationCacheService applicationCacheService)
: BaseBillingCommand<UpgradePremiumToOrganizationCommand>(logger), IUpgradePremiumToOrganizationCommand
{
public Task<BillingCommandResult<None>> Run(
private readonly ILogger<UpgradePremiumToOrganizationCommand> _logger = logger;

public Task<BillingCommandResult<Guid>> Run(
User user,
string organizationName,
string key,
string publicKey,
string encryptedPrivateKey,
string? collectionName,
PlanType targetPlanType,
Payment.Models.BillingAddress billingAddress) => HandleAsync<None>(async () =>
Payment.Models.BillingAddress billingAddress) => HandleAsync<Guid>(async () =>
{
// Validate that the user has an active Premium subscription
if (user is not { Premium: true, GatewaySubscriptionId: not null and not "" })
Expand Down Expand Up @@ -165,6 +177,8 @@ public Task<BillingCommandResult<None>> Run(
Gateway = GatewayType.Stripe,
Enabled = true,
LicenseKey = CoreHelpers.SecureRandomString(20),
PublicKey = publicKey,
PrivateKey = encryptedPrivateKey,
CreationDate = DateTime.UtcNow,
RevisionDate = DateTime.UtcNow,
Status = OrganizationStatusType.Created,
Expand Down Expand Up @@ -218,6 +232,33 @@ await organizationApiKeyRepository.CreateAsync(new OrganizationApiKey
organizationUser.SetNewId();
await organizationUserRepository.CreateAsync(organizationUser);

// Create default collection if collection name is provided
if (!string.IsNullOrWhiteSpace(collectionName))
{
try
{
// Give the owner Can Manage access over the default collection
List<CollectionAccessSelection> defaultOwnerAccess =
[new CollectionAccessSelection { Id = organizationUser.Id, HidePasswords = false, ReadOnly = false, Manage = true }];

var defaultCollection = new Collection
{
Name = collectionName,
OrganizationId = organization.Id,
CreationDate = organization.CreationDate,
RevisionDate = organization.CreationDate
};
await collectionRepository.CreateAsync(defaultCollection, null, defaultOwnerAccess);
}
catch (Exception ex)
{
_logger.LogWarning(ex,
"{Command}: Failed to create default collection for organization {OrganizationId}. Organization upgrade will continue.",
CommandName, organization.Id);
// Continue - organization is fully functional without default collection
}
}

// Remove subscription from user
user.Premium = false;
user.PremiumExpirationDate = null;
Expand All @@ -226,6 +267,6 @@ await organizationApiKeyRepository.CreateAsync(new OrganizationApiKey
user.RevisionDate = DateTime.UtcNow;
await userService.SaveUserAsync(user);

return new None();
return organization.Id;
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ public void ToDomain_ValidTierTypes_ReturnsPlanType(ProductTierType tierType, Pl
{
OrganizationName = "Test Organization",
Key = "encrypted-key",
PublicKey = "public-key",
EncryptedPrivateKey = "encrypted-private-key",
CollectionName = "Default Collection",
TargetProductTierType = tierType,
BillingAddress = new MinimalBillingAddressRequest
{
Expand All @@ -27,11 +30,14 @@ public void ToDomain_ValidTierTypes_ReturnsPlanType(ProductTierType tierType, Pl
};

// Act
var (organizationName, key, planType, billingAddress) = sut.ToDomain();
var (organizationName, key, publicKey, encryptedPrivateKey, collectionName, planType, billingAddress) = sut.ToDomain();

// Assert
Assert.Equal("Test Organization", organizationName);
Assert.Equal("encrypted-key", key);
Assert.Equal("public-key", publicKey);
Assert.Equal("encrypted-private-key", encryptedPrivateKey);
Assert.Equal("Default Collection", collectionName);
Assert.Equal(expectedPlanType, planType);
Assert.Equal("US", billingAddress.Country);
Assert.Equal("12345", billingAddress.PostalCode);
Expand All @@ -47,6 +53,8 @@ public void ToDomain_InvalidTierTypes_ThrowsInvalidOperationException(ProductTie
{
OrganizationName = "Test Organization",
Key = "encrypted-key",
PublicKey = "public-key",
EncryptedPrivateKey = "encrypted-private-key",
TargetProductTierType = tierType,
BillingAddress = new MinimalBillingAddressRequest
{
Expand Down
Loading
Loading