1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-04 17:42:49 -05:00

[AC-2551] Consolidated Billing Migration (#4616)

* Move existing Billing SQL files into dbo folder

I noticed that every other team had a nested dbo folder under their team folder while Billing did not. This change replicates that.

* Add SQL files for ClientOrganizationMigrationRecord table

* Add SQL Server migration for ClientOrganizationMigrationRecord table

* Add ClientOrganizationMigrationRecord entity and repository interface

* Add ClientOrganizationMigrationRecord Dapper repository

* Add ClientOrganizationMigrationRecord EF repository

* Add EF migrations for ClientOrganizationMigrationRecord table

* Implement migration process

* Wire up new Admin tool to migrate providers

* Run dotnet format

* Updated coupon and credit application per product request

* AC-3057-3058: Fix expiration date and enabled from webhook processing

* Run dotnet format

* AC-3059: Fix assigned seats during migration

* Updated AllocatedSeats in the case plan already exists

* Update migration scripts to reflect current date
This commit is contained in:
Alex Morask
2024-10-04 10:55:00 -04:00
committed by GitHub
parent 738febf031
commit 0496085c39
63 changed files with 10418 additions and 3 deletions

View File

@ -0,0 +1,83 @@
using Bit.Admin.Billing.Models;
using Bit.Admin.Enums;
using Bit.Admin.Utilities;
using Bit.Core.Billing.Migration.Models;
using Bit.Core.Billing.Migration.Services;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Admin.Billing.Controllers;
[Authorize]
[Route("migrate-providers")]
[SelfHosted(NotSelfHostedOnly = true)]
public class MigrateProvidersController(
IProviderMigrator providerMigrator) : Controller
{
[HttpGet]
[RequirePermission(Permission.Tools_MigrateProviders)]
public IActionResult Index()
{
return View(new MigrateProvidersRequestModel());
}
[HttpPost]
[RequirePermission(Permission.Tools_MigrateProviders)]
[ValidateAntiForgeryToken]
public async Task<IActionResult> PostAsync(MigrateProvidersRequestModel request)
{
var providerIds = GetProviderIdsFromInput(request.ProviderIds);
if (providerIds.Count == 0)
{
return RedirectToAction("Index");
}
foreach (var providerId in providerIds)
{
await providerMigrator.Migrate(providerId);
}
return RedirectToAction("Results", new { ProviderIds = string.Join("\r\n", providerIds) });
}
[HttpGet("results")]
[RequirePermission(Permission.Tools_MigrateProviders)]
public async Task<IActionResult> ResultsAsync(MigrateProvidersRequestModel request)
{
var providerIds = GetProviderIdsFromInput(request.ProviderIds);
if (providerIds.Count == 0)
{
return View(Array.Empty<ProviderMigrationResult>());
}
var results = await Task.WhenAll(providerIds.Select(providerMigrator.GetResult));
return View(results);
}
[HttpGet("results/{providerId:guid}")]
[RequirePermission(Permission.Tools_MigrateProviders)]
public async Task<IActionResult> DetailsAsync([FromRoute] Guid providerId)
{
var result = await providerMigrator.GetResult(providerId);
if (result == null)
{
return RedirectToAction("Index");
}
return View(result);
}
private static List<Guid> GetProviderIdsFromInput(string text) => !string.IsNullOrEmpty(text)
? text.Split(
["\r\n", "\r", "\n"],
StringSplitOptions.TrimEntries
)
.Select(id => new Guid(id))
.ToList()
: [];
}

View File

@ -0,0 +1,10 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Admin.Billing.Models;
public class MigrateProvidersRequestModel
{
[Required]
[Display(Name = "Provider IDs")]
public string ProviderIds { get; set; }
}

View File

@ -0,0 +1,39 @@
@using System.Text.Json
@model Bit.Core.Billing.Migration.Models.ProviderMigrationResult
@{
ViewData["Title"] = "Results";
}
<h1>Migrate Providers</h1>
<h2>Migration Details: @Model.ProviderName</h2>
<dl class="row">
<dt class="col-sm-4 col-lg-3">Id</dt>
<dd class="col-sm-8 col-lg-9"><code>@Model.ProviderId</code></dd>
<dt class="col-sm-4 col-lg-3">Result</dt>
<dd class="col-sm-8 col-lg-9">@Model.Result</dd>
</dl>
<h3>Client Organizations</h3>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Result</th>
<th>Previous State</th>
</tr>
</thead>
<tbody>
@foreach (var clientResult in Model.Clients)
{
<tr>
<td>@clientResult.OrganizationId</td>
<td>@clientResult.OrganizationName</td>
<td>@clientResult.Result</td>
<td><pre>@Html.Raw(JsonSerializer.Serialize(clientResult.PreviousState))</pre></td>
</tr>
}
</tbody>
</table>
</div>

View File

@ -0,0 +1,46 @@
@model Bit.Admin.Billing.Models.MigrateProvidersRequestModel;
@{
ViewData["Title"] = "Migrate Providers";
}
<h1>Migrate Providers</h1>
<h2>Bulk Consolidated Billing Migration Tool</h2>
<section>
<p>
This tool allows you to provide a list of IDs for Providers that you would like to migrate to Consolidated Billing.
Because of the expensive nature of the operation, you can only migrate 10 Providers at a time.
</p>
<p class="alert alert-warning">
Updates made through this tool are irreversible without manual intervention.
</p>
<p>Example Input (Please enter each Provider ID separated by a new line):</p>
<div class="card">
<div class="card-body">
<pre class="mb-0">f513affc-2290-4336-879e-21ec3ecf3e78
f7a5cb0d-4b74-445c-8d8c-232d1d32bbe2
bf82d3cf-0e21-4f39-b81b-ef52b2fc6a3a
174e82fc-70c3-448d-9fe7-00bad2a3ab00
22a4bbbf-58e3-4e4c-a86a-a0d7caf4ff14</pre>
</div>
</div>
<form method="post" asp-controller="MigrateProviders" asp-action="Post" class="mt-2">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="ProviderIds"></label>
<textarea rows="10" class="form-control" type="text" asp-for="ProviderIds"></textarea>
</div>
<div class="form-group">
<input type="submit" value="Run" class="btn btn-primary mb-2"/>
</div>
</form>
<form method="get" asp-controller="MigrateProviders" asp-action="Results" class="mt-2">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="ProviderIds"></label>
<textarea rows="10" class="form-control" type="text" asp-for="ProviderIds"></textarea>
</div>
<div class="form-group">
<input type="submit" value="See Previous Results" class="btn btn-primary mb-2"/>
</div>
</form>
</section>

View File

@ -0,0 +1,28 @@
@model Bit.Core.Billing.Migration.Models.ProviderMigrationResult[]
@{
ViewData["Title"] = "Results";
}
<h1>Migrate Providers</h1>
<h2>Results</h2>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Result</th>
</tr>
</thead>
<tbody>
@foreach (var result in Model)
{
<tr>
<td><a href="@Url.Action("Details", "MigrateProviders", new { providerId = result.ProviderId })">@result.ProviderId</a></td>
<td>@result.ProviderName</td>
<td>@result.Result</td>
</tr>
}
</tbody>
</table>
</div>

View File

@ -48,5 +48,6 @@ public enum Permission
Tools_ManageTaxRates,
Tools_ManageStripeSubscriptions,
Tools_CreateEditTransaction,
Tools_ProcessStripeEvents
Tools_ProcessStripeEvents,
Tools_MigrateProviders
}

View File

@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Bit.Admin.Services;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Migration;
#if !OSS
using Bit.Commercial.Core.Utilities;
@ -88,8 +89,10 @@ public class Startup
services.AddBaseServices(globalSettings);
services.AddDefaultServices(globalSettings);
services.AddScoped<IAccessControlService, AccessControlService>();
services.AddDistributedCache(globalSettings);
services.AddBillingOperations();
services.AddHttpClient();
services.AddProviderMigration();
#if OSS
services.AddOosServices();

View File

@ -163,6 +163,7 @@ public static class RolePermissionMapping
Permission.Tools_ManageStripeSubscriptions,
Permission.Tools_CreateEditTransaction,
Permission.Tools_ProcessStripeEvents,
Permission.Tools_MigrateProviders
}
},
{ "sales", new List<Permission>

View File

@ -15,6 +15,7 @@
var canManageTaxRates = AccessControlService.UserHasPermission(Permission.Tools_ManageTaxRates);
var canManageStripeSubscriptions = AccessControlService.UserHasPermission(Permission.Tools_ManageStripeSubscriptions);
var canProcessStripeEvents = AccessControlService.UserHasPermission(Permission.Tools_ProcessStripeEvents);
var canMigrateProviders = AccessControlService.UserHasPermission(Permission.Tools_MigrateProviders);
var canViewTools = canChargeBraintree || canCreateTransaction || canPromoteAdmin ||
canGenerateLicense || canManageTaxRates || canManageStripeSubscriptions;
@ -108,12 +109,18 @@
Manage Stripe Subscriptions
</a>
}
@if (canProcessStripeEvents)
@if (canProcessStripeEvents)
{
<a class="dropdown-item" asp-controller="ProcessStripeEvents" asp-action="Index">
Process Stripe Events
</a>
}
@if (canMigrateProviders)
{
<a class="dropdown-item" asp-controller="MigrateProviders" asp-action="index">
Migrate Providers
</a>
}
</div>
</li>
}