Skip to content
Open
34 changes: 32 additions & 2 deletions src/Admin/AdminConsole/Controllers/OrganizationsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
using Bit.Core.AdminConsole.Providers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Utilities.v2;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Organizations.PlanMigration.Entities;
using Bit.Core.Billing.Organizations.PlanMigration.Repositories;
using Bit.Core.Billing.Organizations.PlanMigration.ValueObjects;
Expand All @@ -37,6 +39,7 @@
using Bit.Core.Vault.Repositories;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Stripe;

namespace Bit.Admin.AdminConsole.Controllers;

Expand Down Expand Up @@ -231,8 +234,35 @@ public async Task<IActionResult> Edit(Guid id)
policies = await _policyRepository.GetManyByOrganizationIdAsync(id);
}
var users = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(id);
var billingInfo = await _paymentService.GetBillingAsync(organization);
var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(organization);
BillingInfo billingInfo = null;
BillingHistoryInfo billingHistoryInfo = null;
try
{
billingInfo = await _paymentService.GetBillingAsync(organization);
billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(organization);
}
catch (StripeException ex) when (ex.StripeError?.Code == StripeConstants.ErrorCodes.ResourceMissing)
{
billingInfo = null;
billingHistoryInfo = null;
_logger.LogError(ex,
"Billing information for organization {OrganizationId} could not be loaded because the Stripe customer was not found. It may have been deleted.",
id);
TempData["Warning"] =
"Billing information could not be loaded. The Stripe customer may have been deleted. " +
"You can still edit the organization and set a valid Gateway Customer ID.";
}
catch (Exception ex)
{
billingInfo = null;
billingHistoryInfo = null;
_logger.LogError(ex,
"Failed to load billing information for organization {OrganizationId}.",
id);
TempData["Error"] =
"Billing information could not be loaded. You can still edit the organization or try reloading the page. " +
"Contact support if the problem persists.";
}
Comment thread
kdenney marked this conversation as resolved.
Dismissed
var billingSyncConnection = _globalSettings.EnableCloudCommunication ? await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(id, OrganizationConnectionType.CloudBillingSync) : null;
var secrets = organization.UseSecretsManager ? await _secretRepository.GetSecretsCountByOrganizationIdAsync(id) : -1;
var projects = organization.UseSecretsManager ? await _projectRepository.GetProjectCountByOrganizationIdAsync(id) : -1;
Expand Down
2 changes: 1 addition & 1 deletion src/Admin/AdminConsole/Views/Organizations/Edit.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
@await Html.PartialAsync("_ViewInformation", Model)
}

@if (canViewBillingInformation)
@if (canViewBillingInformation && Model.BillingInfo != null && Model.BillingHistoryInfo != null)
{
<h2>Billing Information</h2>
@await Html.PartialAsync("_BillingInformation",
Expand Down
8 changes: 8 additions & 0 deletions src/Admin/Views/Shared/_Layout.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,14 @@
});
</script>
}
@if (TempData["Warning"] != null)
{
<script>
$(document).ready(function () {
toastr.warning("@TempData["Warning"]")
});
</script>
}

@RenderSection("Scripts", required: false)
</body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Organizations.PlanMigration.Entities;
using Bit.Core.Billing.Organizations.PlanMigration.Enums;
using Bit.Core.Billing.Organizations.PlanMigration.Repositories;
using Bit.Core.Billing.Providers.Services;
using Bit.Core.Billing.Services;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Core.Services;
Expand All @@ -24,6 +27,7 @@
using Microsoft.EntityFrameworkCore;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Stripe;
using static Bit.Core.AdminConsole.Utilities.v2.Validation.ValidationResultHelpers;

namespace Admin.Test.AdminConsole.Controllers;
Expand Down Expand Up @@ -1911,5 +1915,111 @@ await sutProvider.GetDependency<IOrganizationPlanMigrationCohortAssignmentReposi
.CreateAsync(default);
}

[BitAutoData]
[SutProviderCustomize]
[Theory]
public async Task Edit_Get_BillingLoadThrows_StillRendersPageWithWarning(
Organization organization,
SutProvider<OrganizationsController> sutProvider)
{
// PM-38874: a deleted Stripe customer makes GetBillingAsync throw. The page must still
// render so an admin can correct the Gateway Customer ID rather than being locked out.
StubEditGetDependencies(sutProvider, organization, currentAssignment: null);

sutProvider.GetDependency<IStripePaymentService>()
.GetBillingAsync(organization)
.ThrowsAsync(new StripeException
{
StripeError = new StripeError { Code = StripeConstants.ErrorCodes.ResourceMissing }
});

sutProvider.Sut.TempData =
new TempDataDictionary(new DefaultHttpContext(), Substitute.For<ITempDataProvider>());

var result = await sutProvider.Sut.Edit(organization.Id);

var view = Assert.IsType<ViewResult>(result);
var model = Assert.IsType<OrganizationEditModel>(view.Model);
Assert.Null(model.BillingInfo);
Assert.Null(model.BillingHistoryInfo);
Assert.True(sutProvider.Sut.TempData.ContainsKey("Warning"));
Assert.Equal(
"Billing information could not be loaded. The Stripe customer may have been deleted. " +
"You can still edit the organization and set a valid Gateway Customer ID.",
(string)sutProvider.Sut.TempData["Warning"]);
}

[BitAutoData]
[SutProviderCustomize]
[Theory]
public async Task Edit_Get_BillingHistoryLoadThrows_StillRendersPageWithWarning(
Organization organization,
BillingInfo billingInfo,
SutProvider<OrganizationsController> sutProvider)
{
// PM-38874: GetBillingAsync can succeed while GetBillingHistoryAsync throws. The catch must
// reset both values so the billing section is hidden and the page renders, rather than
// falling through with a non-null BillingInfo and a null BillingHistoryInfo (which would NRE).
StubEditGetDependencies(sutProvider, organization, currentAssignment: null);

sutProvider.GetDependency<IStripePaymentService>()
.GetBillingAsync(organization)
.Returns(billingInfo);
sutProvider.GetDependency<IStripePaymentService>()
.GetBillingHistoryAsync(organization)
.ThrowsAsync(new StripeException
{
StripeError = new StripeError { Code = StripeConstants.ErrorCodes.ResourceMissing }
});

sutProvider.Sut.TempData =
new TempDataDictionary(new DefaultHttpContext(), Substitute.For<ITempDataProvider>());

var result = await sutProvider.Sut.Edit(organization.Id);

var view = Assert.IsType<ViewResult>(result);
var model = Assert.IsType<OrganizationEditModel>(view.Model);
Assert.Null(model.BillingInfo);
Assert.Null(model.BillingHistoryInfo);
Assert.True(sutProvider.Sut.TempData.ContainsKey("Warning"));
Assert.Equal(
"Billing information could not be loaded. The Stripe customer may have been deleted. " +
"You can still edit the organization and set a valid Gateway Customer ID.",
(string)sutProvider.Sut.TempData["Warning"]);
}

[BitAutoData]
[SutProviderCustomize]
[Theory]
public async Task Edit_Get_BillingLoadThrowsUnexpectedError_StillRendersPageWithErrorToast(
Organization organization,
SutProvider<OrganizationsController> sutProvider)
{
// PM-38874: a billing-load failure that is NOT a missing Stripe customer (resource_missing)
// must fall through to the generic catch, which surfaces a neutral error toast rather than
// asserting the customer was deleted.
StubEditGetDependencies(sutProvider, organization, currentAssignment: null);

sutProvider.GetDependency<IStripePaymentService>()
.GetBillingAsync(organization)
.ThrowsAsync(new StripeException { StripeError = new StripeError { Code = "api_error" } });

sutProvider.Sut.TempData =
new TempDataDictionary(new DefaultHttpContext(), Substitute.For<ITempDataProvider>());

var result = await sutProvider.Sut.Edit(organization.Id);

var view = Assert.IsType<ViewResult>(result);
var model = Assert.IsType<OrganizationEditModel>(view.Model);
Assert.Null(model.BillingInfo);
Assert.Null(model.BillingHistoryInfo);
Assert.False(sutProvider.Sut.TempData.ContainsKey("Warning"));
Assert.True(sutProvider.Sut.TempData.ContainsKey("Error"));
Assert.Equal(
"Billing information could not be loaded. You can still edit the organization or try reloading the page. " +
"Contact support if the problem persists.",
(string)sutProvider.Sut.TempData["Error"]);
}

#endregion
}
Loading