mirror of
https://github.com/bitwarden/server.git
synced 2026-01-16 23:01:09 +00:00
[PM-27117] Sync Stripe Customer details for Organizations and Providers in API & Admin (#6679)
Some checks failed
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
Database testing / Run tests (push) Has been cancelled
Database testing / Run validation (push) Has been cancelled
Database testing / Validate new migration naming and order (push) Has been cancelled
Some checks failed
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
Database testing / Run tests (push) Has been cancelled
Database testing / Run validation (push) Has been cancelled
Database testing / Validate new migration naming and order (push) Has been cancelled
* Sync Stripe customer details for Provider / Organization in API & Admin * Remove unnecessary var * Fix logical operator * Remove customer ID check from callers * Fix failing tests * Missed conflicts
This commit is contained in:
parent
2ecd6c8d5f
commit
39a6719361
11 changed files with 377 additions and 34 deletions
|
|
@ -796,6 +796,44 @@ public class ProviderBillingService(
|
|||
}
|
||||
}
|
||||
|
||||
public async Task UpdateProviderNameAndEmail(Provider provider)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(provider.GatewayCustomerId))
|
||||
{
|
||||
logger.LogWarning(
|
||||
"Provider ({ProviderId}) has no Stripe customer to update",
|
||||
provider.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
var newDisplayName = provider.DisplayName();
|
||||
|
||||
// Provider.DisplayName() can return null - handle gracefully
|
||||
if (string.IsNullOrWhiteSpace(newDisplayName))
|
||||
{
|
||||
logger.LogWarning(
|
||||
"Provider ({ProviderId}) has no name to update in Stripe",
|
||||
provider.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
await stripeAdapter.UpdateCustomerAsync(provider.GatewayCustomerId,
|
||||
new CustomerUpdateOptions
|
||||
{
|
||||
Email = provider.BillingEmail,
|
||||
Description = newDisplayName,
|
||||
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
||||
{
|
||||
CustomFields = [
|
||||
new CustomerInvoiceSettingsCustomFieldOptions
|
||||
{
|
||||
Name = provider.SubscriberType(),
|
||||
Value = newDisplayName
|
||||
}]
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private Func<int, Task> CurrySeatScalingUpdate(
|
||||
Provider provider,
|
||||
ProviderPlan providerPlan,
|
||||
|
|
|
|||
|
|
@ -2150,4 +2150,151 @@ public class ProviderBillingServiceTests
|
|||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region UpdateProviderNameAndEmail
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateProviderNameAndEmail_NullGatewayCustomerId_LogsWarningAndReturns(
|
||||
Provider provider,
|
||||
SutProvider<ProviderBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
provider.GatewayCustomerId = null;
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpdateProviderNameAndEmail(provider);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.DidNotReceive().UpdateCustomerAsync(
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<CustomerUpdateOptions>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateProviderNameAndEmail_EmptyGatewayCustomerId_LogsWarningAndReturns(
|
||||
Provider provider,
|
||||
SutProvider<ProviderBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
provider.GatewayCustomerId = "";
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpdateProviderNameAndEmail(provider);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.DidNotReceive().UpdateCustomerAsync(
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<CustomerUpdateOptions>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateProviderNameAndEmail_NullProviderName_LogsWarningAndReturns(
|
||||
Provider provider,
|
||||
SutProvider<ProviderBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
provider.Name = null;
|
||||
provider.GatewayCustomerId = "cus_test123";
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpdateProviderNameAndEmail(provider);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.DidNotReceive().UpdateCustomerAsync(
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<CustomerUpdateOptions>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateProviderNameAndEmail_EmptyProviderName_LogsWarningAndReturns(
|
||||
Provider provider,
|
||||
SutProvider<ProviderBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
provider.Name = "";
|
||||
provider.GatewayCustomerId = "cus_test123";
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpdateProviderNameAndEmail(provider);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.DidNotReceive().UpdateCustomerAsync(
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<CustomerUpdateOptions>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateProviderNameAndEmail_ValidProvider_CallsStripeWithCorrectParameters(
|
||||
Provider provider,
|
||||
SutProvider<ProviderBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
provider.Name = "Test Provider";
|
||||
provider.BillingEmail = "billing@test.com";
|
||||
provider.GatewayCustomerId = "cus_test123";
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpdateProviderNameAndEmail(provider);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.Received(1).UpdateCustomerAsync(
|
||||
provider.GatewayCustomerId,
|
||||
Arg.Is<CustomerUpdateOptions>(options =>
|
||||
options.Email == provider.BillingEmail &&
|
||||
options.Description == provider.Name &&
|
||||
options.InvoiceSettings.CustomFields.Count == 1 &&
|
||||
options.InvoiceSettings.CustomFields[0].Name == "Provider" &&
|
||||
options.InvoiceSettings.CustomFields[0].Value == provider.Name));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateProviderNameAndEmail_LongProviderName_UsesFullName(
|
||||
Provider provider,
|
||||
SutProvider<ProviderBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var longName = new string('A', 50); // 50 characters
|
||||
provider.Name = longName;
|
||||
provider.BillingEmail = "billing@test.com";
|
||||
provider.GatewayCustomerId = "cus_test123";
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpdateProviderNameAndEmail(provider);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.Received(1).UpdateCustomerAsync(
|
||||
provider.GatewayCustomerId,
|
||||
Arg.Is<CustomerUpdateOptions>(options =>
|
||||
options.InvoiceSettings.CustomFields[0].Value == longName));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateProviderNameAndEmail_NullBillingEmail_UpdatesWithNull(
|
||||
Provider provider,
|
||||
SutProvider<ProviderBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
provider.Name = "Test Provider";
|
||||
provider.BillingEmail = null;
|
||||
provider.GatewayCustomerId = "cus_test123";
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpdateProviderNameAndEmail(provider);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.Received(1).UpdateCustomerAsync(
|
||||
provider.GatewayCustomerId,
|
||||
Arg.Is<CustomerUpdateOptions>(options =>
|
||||
options.Email == null &&
|
||||
options.Description == provider.Name));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ using Bit.Core.AdminConsole.Providers.Interfaces;
|
|||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Organizations.Services;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Providers.Services;
|
||||
using Bit.Core.Billing.Services;
|
||||
|
|
@ -57,6 +58,7 @@ public class OrganizationsController : Controller
|
|||
private readonly IOrganizationInitiateDeleteCommand _organizationInitiateDeleteCommand;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
private readonly IResendOrganizationInviteCommand _resendOrganizationInviteCommand;
|
||||
private readonly IOrganizationBillingService _organizationBillingService;
|
||||
|
||||
public OrganizationsController(
|
||||
IOrganizationRepository organizationRepository,
|
||||
|
|
@ -81,7 +83,8 @@ public class OrganizationsController : Controller
|
|||
IProviderBillingService providerBillingService,
|
||||
IOrganizationInitiateDeleteCommand organizationInitiateDeleteCommand,
|
||||
IPricingClient pricingClient,
|
||||
IResendOrganizationInviteCommand resendOrganizationInviteCommand)
|
||||
IResendOrganizationInviteCommand resendOrganizationInviteCommand,
|
||||
IOrganizationBillingService organizationBillingService)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
|
|
@ -106,6 +109,7 @@ public class OrganizationsController : Controller
|
|||
_organizationInitiateDeleteCommand = organizationInitiateDeleteCommand;
|
||||
_pricingClient = pricingClient;
|
||||
_resendOrganizationInviteCommand = resendOrganizationInviteCommand;
|
||||
_organizationBillingService = organizationBillingService;
|
||||
}
|
||||
|
||||
[RequirePermission(Permission.Org_List_View)]
|
||||
|
|
@ -242,6 +246,8 @@ public class OrganizationsController : Controller
|
|||
var existingOrganizationData = new Organization
|
||||
{
|
||||
Id = organization.Id,
|
||||
Name = organization.Name,
|
||||
BillingEmail = organization.BillingEmail,
|
||||
Status = organization.Status,
|
||||
PlanType = organization.PlanType,
|
||||
Seats = organization.Seats
|
||||
|
|
@ -287,6 +293,22 @@ public class OrganizationsController : Controller
|
|||
|
||||
await _applicationCacheService.UpsertOrganizationAbilityAsync(organization);
|
||||
|
||||
// Sync name/email changes to Stripe
|
||||
if (existingOrganizationData.Name != organization.Name || existingOrganizationData.BillingEmail != organization.BillingEmail)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _organizationBillingService.UpdateOrganizationNameAndEmail(organization);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex,
|
||||
"Failed to update Stripe customer for organization {OrganizationId}. Database was updated successfully.",
|
||||
organization.Id);
|
||||
TempData["Warning"] = "Organization updated successfully, but Stripe customer name/email synchronization failed.";
|
||||
}
|
||||
}
|
||||
|
||||
return RedirectToAction("Edit", new { id });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ public class ProvidersController : Controller
|
|||
private readonly IStripeAdapter _stripeAdapter;
|
||||
private readonly IAccessControlService _accessControlService;
|
||||
private readonly ISubscriberService _subscriberService;
|
||||
private readonly ILogger<ProvidersController> _logger;
|
||||
|
||||
public ProvidersController(IOrganizationRepository organizationRepository,
|
||||
IResellerClientOrganizationSignUpCommand resellerClientOrganizationSignUpCommand,
|
||||
|
|
@ -72,7 +73,8 @@ public class ProvidersController : Controller
|
|||
IPricingClient pricingClient,
|
||||
IStripeAdapter stripeAdapter,
|
||||
IAccessControlService accessControlService,
|
||||
ISubscriberService subscriberService)
|
||||
ISubscriberService subscriberService,
|
||||
ILogger<ProvidersController> logger)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_resellerClientOrganizationSignUpCommand = resellerClientOrganizationSignUpCommand;
|
||||
|
|
@ -92,6 +94,7 @@ public class ProvidersController : Controller
|
|||
_braintreeMerchantUrl = webHostEnvironment.GetBraintreeMerchantUrl();
|
||||
_braintreeMerchantId = globalSettings.Braintree.MerchantId;
|
||||
_subscriberService = subscriberService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[RequirePermission(Permission.Provider_List_View)]
|
||||
|
|
@ -296,6 +299,9 @@ public class ProvidersController : Controller
|
|||
|
||||
var originalProviderStatus = provider.Enabled;
|
||||
|
||||
// Capture original billing email before modifications for Stripe sync
|
||||
var originalBillingEmail = provider.BillingEmail;
|
||||
|
||||
model.ToProvider(provider);
|
||||
|
||||
// validate the stripe ids to prevent saving a bad one
|
||||
|
|
@ -321,6 +327,22 @@ public class ProvidersController : Controller
|
|||
await _providerService.UpdateAsync(provider);
|
||||
await _applicationCacheService.UpsertProviderAbilityAsync(provider);
|
||||
|
||||
// Sync billing email changes to Stripe
|
||||
if (!string.IsNullOrEmpty(provider.GatewayCustomerId) && originalBillingEmail != provider.BillingEmail)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _providerBillingService.UpdateProviderNameAndEmail(provider);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex,
|
||||
"Failed to update Stripe customer for provider {ProviderId}. Database was updated successfully.",
|
||||
provider.Id);
|
||||
TempData["Warning"] = "Provider updated successfully, but Stripe customer email synchronization failed.";
|
||||
}
|
||||
}
|
||||
|
||||
if (!provider.IsBillable())
|
||||
{
|
||||
return RedirectToAction("Edit", new { id });
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using Bit.Api.AdminConsole.Models.Request.Providers;
|
|||
using Bit.Api.AdminConsole.Models.Response.Providers;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Providers.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Services;
|
||||
|
|
@ -23,15 +24,20 @@ public class ProvidersController : Controller
|
|||
private readonly IProviderService _providerService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IProviderBillingService _providerBillingService;
|
||||
private readonly ILogger<ProvidersController> _logger;
|
||||
|
||||
public ProvidersController(IUserService userService, IProviderRepository providerRepository,
|
||||
IProviderService providerService, ICurrentContext currentContext, GlobalSettings globalSettings)
|
||||
IProviderService providerService, ICurrentContext currentContext, GlobalSettings globalSettings,
|
||||
IProviderBillingService providerBillingService, ILogger<ProvidersController> logger)
|
||||
{
|
||||
_userService = userService;
|
||||
_providerRepository = providerRepository;
|
||||
_providerService = providerService;
|
||||
_currentContext = currentContext;
|
||||
_globalSettings = globalSettings;
|
||||
_providerBillingService = providerBillingService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
|
|
@ -65,7 +71,27 @@ public class ProvidersController : Controller
|
|||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// Capture original values before modifications for Stripe sync
|
||||
var originalName = provider.Name;
|
||||
var originalBillingEmail = provider.BillingEmail;
|
||||
|
||||
await _providerService.UpdateAsync(model.ToProvider(provider, _globalSettings));
|
||||
|
||||
// Sync name/email changes to Stripe
|
||||
if (originalName != provider.Name || originalBillingEmail != provider.BillingEmail)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _providerBillingService.UpdateProviderNameAndEmail(provider);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex,
|
||||
"Failed to update Stripe customer for provider {ProviderId}. Database was updated successfully.",
|
||||
provider.Id);
|
||||
}
|
||||
}
|
||||
|
||||
return new ProviderResponseModel(provider);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ public class OrganizationUpdateCommand(
|
|||
var shouldUpdateBilling = originalName != organization.Name ||
|
||||
originalBillingEmail != organization.BillingEmail;
|
||||
|
||||
if (!shouldUpdateBilling || string.IsNullOrWhiteSpace(organization.GatewayCustomerId))
|
||||
if (!shouldUpdateBilling)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,10 +61,6 @@ public interface IOrganizationBillingService
|
|||
/// Updates the organization name and email on the Stripe customer entry.
|
||||
/// This only updates Stripe, not the Bitwarden database.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The caller should ensure that the organization has a GatewayCustomerId before calling this method.
|
||||
/// </remarks>
|
||||
/// <param name="organization">The organization to update in Stripe.</param>
|
||||
/// <exception cref="BillingException">Thrown when the organization does not have a GatewayCustomerId.</exception>
|
||||
Task UpdateOrganizationNameAndEmail(Organization organization);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -177,13 +177,25 @@ public class OrganizationBillingService(
|
|||
|
||||
public async Task UpdateOrganizationNameAndEmail(Organization organization)
|
||||
{
|
||||
if (organization.GatewayCustomerId is null)
|
||||
if (string.IsNullOrWhiteSpace(organization.GatewayCustomerId))
|
||||
{
|
||||
throw new BillingException("Cannot update an organization in Stripe without a GatewayCustomerId.");
|
||||
logger.LogWarning(
|
||||
"Organization ({OrganizationId}) has no Stripe customer to update",
|
||||
organization.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
var newDisplayName = organization.DisplayName();
|
||||
|
||||
// Organization.DisplayName() can return null - handle gracefully
|
||||
if (string.IsNullOrWhiteSpace(newDisplayName))
|
||||
{
|
||||
logger.LogWarning(
|
||||
"Organization ({OrganizationId}) has no name to update in Stripe",
|
||||
organization.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
await stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId,
|
||||
new CustomerUpdateOptions
|
||||
{
|
||||
|
|
@ -196,9 +208,7 @@ public class OrganizationBillingService(
|
|||
new CustomerInvoiceSettingsCustomFieldOptions
|
||||
{
|
||||
Name = organization.SubscriberType(),
|
||||
Value = newDisplayName.Length <= 30
|
||||
? newDisplayName
|
||||
: newDisplayName[..30]
|
||||
Value = newDisplayName
|
||||
}]
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -113,4 +113,11 @@ public interface IProviderBillingService
|
|||
TaxInformation taxInformation);
|
||||
|
||||
Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the provider name and email on the Stripe customer entry.
|
||||
/// This only updates Stripe, not the Bitwarden database.
|
||||
/// </summary>
|
||||
/// <param name="provider">The provider to update in Stripe.</param>
|
||||
Task UpdateProviderNameAndEmail(Provider provider);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ public class OrganizationUpdateCommandTests
|
|||
var organizationBillingService = sutProvider.GetDependency<IOrganizationBillingService>();
|
||||
|
||||
organization.Id = organizationId;
|
||||
organization.GatewayCustomerId = null; // No Stripe customer, so no billing update
|
||||
organization.GatewayCustomerId = null; // No Stripe customer, but billing update is still called
|
||||
|
||||
organizationRepository
|
||||
.GetByIdAsync(organizationId)
|
||||
|
|
@ -61,8 +61,8 @@ public class OrganizationUpdateCommandTests
|
|||
result,
|
||||
EventType.Organization_Updated);
|
||||
await organizationBillingService
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.UpdateOrganizationNameAndEmail(Arg.Any<Organization>());
|
||||
.Received(1)
|
||||
.UpdateOrganizationNameAndEmail(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
|
|
@ -93,7 +93,7 @@ public class OrganizationUpdateCommandTests
|
|||
[Theory]
|
||||
[BitAutoData("")]
|
||||
[BitAutoData((string)null)]
|
||||
public async Task UpdateAsync_WhenGatewayCustomerIdIsNullOrEmpty_SkipsBillingUpdate(
|
||||
public async Task UpdateAsync_WhenGatewayCustomerIdIsNullOrEmpty_CallsBillingUpdateButHandledGracefully(
|
||||
string gatewayCustomerId,
|
||||
Guid organizationId,
|
||||
Organization organization,
|
||||
|
|
@ -133,8 +133,8 @@ public class OrganizationUpdateCommandTests
|
|||
result,
|
||||
EventType.Organization_Updated);
|
||||
await organizationBillingService
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.UpdateOrganizationNameAndEmail(Arg.Any<Organization>());
|
||||
.Received(1)
|
||||
.UpdateOrganizationNameAndEmail(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models.Sales;
|
||||
|
|
@ -391,12 +390,13 @@ public class OrganizationBillingServiceTests
|
|||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateOrganizationNameAndEmail_WhenNameIsLong_TruncatesTo30Characters(
|
||||
public async Task UpdateOrganizationNameAndEmail_WhenNameIsLong_UsesFullName(
|
||||
Organization organization,
|
||||
SutProvider<OrganizationBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organization.Name = "This is a very long organization name that exceeds thirty characters";
|
||||
var longName = "This is a very long organization name that exceeds thirty characters";
|
||||
organization.Name = longName;
|
||||
|
||||
CustomerUpdateOptions capturedOptions = null;
|
||||
sutProvider.GetDependency<IStripeAdapter>()
|
||||
|
|
@ -420,14 +420,11 @@ public class OrganizationBillingServiceTests
|
|||
Assert.NotNull(capturedOptions.InvoiceSettings.CustomFields);
|
||||
|
||||
var customField = capturedOptions.InvoiceSettings.CustomFields.First();
|
||||
Assert.Equal(30, customField.Value.Length);
|
||||
|
||||
var expectedCustomFieldDisplayName = "This is a very long organizati";
|
||||
Assert.Equal(expectedCustomFieldDisplayName, customField.Value);
|
||||
Assert.Equal(longName, customField.Value);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateOrganizationNameAndEmail_WhenGatewayCustomerIdIsNull_ThrowsBillingException(
|
||||
public async Task UpdateOrganizationNameAndEmail_WhenGatewayCustomerIdIsNull_LogsWarningAndReturns(
|
||||
Organization organization,
|
||||
SutProvider<OrganizationBillingService> sutProvider)
|
||||
{
|
||||
|
|
@ -435,15 +432,93 @@ public class OrganizationBillingServiceTests
|
|||
organization.GatewayCustomerId = null;
|
||||
organization.Name = "Test Organization";
|
||||
organization.BillingEmail = "billing@example.com";
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BillingException>(
|
||||
() => sutProvider.Sut.UpdateOrganizationNameAndEmail(organization));
|
||||
// Act
|
||||
await sutProvider.Sut.UpdateOrganizationNameAndEmail(organization);
|
||||
|
||||
Assert.Contains("Cannot update an organization in Stripe without a GatewayCustomerId.", exception.Response);
|
||||
// Assert
|
||||
await stripeAdapter.DidNotReceive().UpdateCustomerAsync(
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<CustomerUpdateOptions>());
|
||||
}
|
||||
|
||||
await sutProvider.GetDependency<IStripeAdapter>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>());
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateOrganizationNameAndEmail_WhenGatewayCustomerIdIsEmpty_LogsWarningAndReturns(
|
||||
Organization organization,
|
||||
SutProvider<OrganizationBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organization.GatewayCustomerId = "";
|
||||
organization.Name = "Test Organization";
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpdateOrganizationNameAndEmail(organization);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.DidNotReceive().UpdateCustomerAsync(
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<CustomerUpdateOptions>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateOrganizationNameAndEmail_WhenNameIsNull_LogsWarningAndReturns(
|
||||
Organization organization,
|
||||
SutProvider<OrganizationBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organization.Name = null;
|
||||
organization.GatewayCustomerId = "cus_test123";
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpdateOrganizationNameAndEmail(organization);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.DidNotReceive().UpdateCustomerAsync(
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<CustomerUpdateOptions>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateOrganizationNameAndEmail_WhenNameIsEmpty_LogsWarningAndReturns(
|
||||
Organization organization,
|
||||
SutProvider<OrganizationBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organization.Name = "";
|
||||
organization.GatewayCustomerId = "cus_test123";
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpdateOrganizationNameAndEmail(organization);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.DidNotReceive().UpdateCustomerAsync(
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<CustomerUpdateOptions>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateOrganizationNameAndEmail_WhenBillingEmailIsNull_UpdatesWithNull(
|
||||
Organization organization,
|
||||
SutProvider<OrganizationBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organization.Name = "Test Organization";
|
||||
organization.BillingEmail = null;
|
||||
organization.GatewayCustomerId = "cus_test123";
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpdateOrganizationNameAndEmail(organization);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.Received(1).UpdateCustomerAsync(
|
||||
organization.GatewayCustomerId,
|
||||
Arg.Is<CustomerUpdateOptions>(options =>
|
||||
options.Email == null &&
|
||||
options.Description == organization.Name));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue