1
0
mirror of https://github.com/bitwarden/server synced 2025-12-24 20:23:21 +00:00

[PM-19566] Update MSPs to "charge_automatically" with Admin-based opt-out (#5650)

* Update provider to charge automatically with Admin Portal-based opt-out

* Design feedback

* Run dotnet format
This commit is contained in:
Alex Morask
2025-04-16 13:36:04 -04:00
committed by GitHub
parent 3d59f5522d
commit 01a08c5814
9 changed files with 163 additions and 4 deletions

View File

@@ -3,11 +3,13 @@ using System.Net;
using Bit.Admin.AdminConsole.Models;
using Bit.Admin.Enums;
using Bit.Admin.Utilities;
using Bit.Core;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Providers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
@@ -23,6 +25,7 @@ using Bit.Core.Settings;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Stripe;
namespace Bit.Admin.AdminConsole.Controllers;
@@ -44,6 +47,7 @@ public class ProvidersController : Controller
private readonly IProviderPlanRepository _providerPlanRepository;
private readonly IProviderBillingService _providerBillingService;
private readonly IPricingClient _pricingClient;
private readonly IStripeAdapter _stripeAdapter;
private readonly string _stripeUrl;
private readonly string _braintreeMerchantUrl;
private readonly string _braintreeMerchantId;
@@ -63,7 +67,8 @@ public class ProvidersController : Controller
IProviderPlanRepository providerPlanRepository,
IProviderBillingService providerBillingService,
IWebHostEnvironment webHostEnvironment,
IPricingClient pricingClient)
IPricingClient pricingClient,
IStripeAdapter stripeAdapter)
{
_organizationRepository = organizationRepository;
_organizationService = organizationService;
@@ -79,6 +84,7 @@ public class ProvidersController : Controller
_providerPlanRepository = providerPlanRepository;
_providerBillingService = providerBillingService;
_pricingClient = pricingClient;
_stripeAdapter = stripeAdapter;
_stripeUrl = webHostEnvironment.GetStripeUrl();
_braintreeMerchantUrl = webHostEnvironment.GetBraintreeMerchantUrl();
_braintreeMerchantId = globalSettings.Braintree.MerchantId;
@@ -306,6 +312,23 @@ public class ProvidersController : Controller
(Plan: PlanType.EnterpriseMonthly, SeatsMinimum: model.EnterpriseMonthlySeatMinimum)
]);
await _providerBillingService.UpdateSeatMinimums(updateMspSeatMinimumsCommand);
if (_featureService.IsEnabled(FeatureFlagKeys.PM199566_UpdateMSPToChargeAutomatically))
{
var customer = await _stripeAdapter.CustomerGetAsync(provider.GatewayCustomerId);
if (model.PayByInvoice != customer.ApprovedToPayByInvoice())
{
var approvedToPayByInvoice = model.PayByInvoice ? "1" : "0";
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
{
Metadata = new Dictionary<string, string>
{
[StripeConstants.MetadataKeys.InvoiceApproved] = approvedToPayByInvoice
}
});
}
}
break;
case ProviderType.BusinessUnit:
{
@@ -345,14 +368,18 @@ public class ProvidersController : Controller
if (!provider.IsBillable())
{
return new ProviderEditModel(provider, users, providerOrganizations, new List<ProviderPlan>());
return new ProviderEditModel(provider, users, providerOrganizations, new List<ProviderPlan>(), false);
}
var providerPlans = await _providerPlanRepository.GetByProviderId(id);
var payByInvoice =
_featureService.IsEnabled(FeatureFlagKeys.PM199566_UpdateMSPToChargeAutomatically) &&
(await _stripeAdapter.CustomerGetAsync(provider.GatewayCustomerId)).ApprovedToPayByInvoice();
return new ProviderEditModel(
provider, users, providerOrganizations,
providerPlans.ToList(), GetGatewayCustomerUrl(provider), GetGatewaySubscriptionUrl(provider));
providerPlans.ToList(), payByInvoice, GetGatewayCustomerUrl(provider), GetGatewaySubscriptionUrl(provider));
}
[RequirePermission(Permission.Provider_ResendEmailInvite)]

View File

@@ -18,6 +18,7 @@ public class ProviderEditModel : ProviderViewModel, IValidatableObject
IEnumerable<ProviderUserUserDetails> providerUsers,
IEnumerable<ProviderOrganizationOrganizationDetails> organizations,
IReadOnlyCollection<ProviderPlan> providerPlans,
bool payByInvoice,
string gatewayCustomerUrl = null,
string gatewaySubscriptionUrl = null) : base(provider, providerUsers, organizations, providerPlans)
{
@@ -33,6 +34,7 @@ public class ProviderEditModel : ProviderViewModel, IValidatableObject
GatewayCustomerUrl = gatewayCustomerUrl;
GatewaySubscriptionUrl = gatewaySubscriptionUrl;
Type = provider.Type;
PayByInvoice = payByInvoice;
if (Type == ProviderType.BusinessUnit)
{
@@ -62,6 +64,8 @@ public class ProviderEditModel : ProviderViewModel, IValidatableObject
public string GatewaySubscriptionId { get; set; }
public string GatewayCustomerUrl { get; }
public string GatewaySubscriptionUrl { get; }
[Display(Name = "Pay By Invoice")]
public bool PayByInvoice { get; set; }
[Display(Name = "Provider Type")]
public ProviderType Type { get; set; }

View File

@@ -136,6 +136,17 @@
</div>
</div>
</div>
@if (FeatureService.IsEnabled(FeatureFlagKeys.PM199566_UpdateMSPToChargeAutomatically) && Model.Provider.Type == ProviderType.Msp && Model.Provider.IsBillable())
{
<div class="row">
<div class="col-sm">
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" asp-for="PayByInvoice">
<label class="form-check-label" asp-for="PayByInvoice"></label>
</div>
</div>
</div>
}
}
</form>
@await Html.PartialAsync("Organizations", Model)