mirror of
https://github.com/bitwarden/server
synced 2025-12-30 15:14:02 +00:00
[PM-21638] Stripe .NET v48 (#6202)
* Upgrade Stripe.net to v48.4.0 * Update PreviewTaxAmountCommand * Remove unused UpcomingInvoiceOptionExtensions * Added SubscriptionExtensions with GetCurrentPeriodEnd * Update PremiumUserBillingService * Update OrganizationBillingService * Update GetOrganizationWarningsQuery * Update BillingHistoryInfo * Update SubscriptionInfo * Remove unused Sql Billing folder * Update StripeAdapter * Update StripePaymentService * Update InvoiceCreatedHandler * Update PaymentFailedHandler * Update PaymentSucceededHandler * Update ProviderEventService * Update StripeEventUtilityService * Update SubscriptionDeletedHandler * Update SubscriptionUpdatedHandler * Update UpcomingInvoiceHandler * Update ProviderSubscriptionResponse * Remove unused Stripe Subscriptions Admin Tool * Update RemoveOrganizationFromProviderCommand * Update ProviderBillingService * Update RemoveOrganizatinoFromProviderCommandTests * Update PreviewTaxAmountCommandTests * Update GetCloudOrganizationLicenseQueryTests * Update GetOrganizationWarningsQueryTests * Update StripePaymentServiceTests * Update ProviderBillingControllerTests * Update ProviderEventServiceTests * Update SubscriptionDeletedHandlerTests * Update SubscriptionUpdatedHandlerTests * Resolve Billing test failures I completely removed tests for the StripeEventService as they were using a system I setup a while back that read JSON files of the Stripe event structure. I did not anticipate how frequently these structures would change with each API version and the cost of trying to update these specific JSON files to test a very static data retrieval service far outweigh the benefit. * Resolve Core test failures * Run dotnet format * Remove unused provider migration * Fixed failing tests * Run dotnet format * Replace the old webhook secret key with new one (#6223) * Fix compilation failures in additions * Run dotnet format * Bump Stripe API version * Fix recent addition: CreatePremiumCloudHostedSubscriptionCommand * Fix new code in main according to Stripe update * Fix InvoiceExtensions * Bump SDK version to match API Version * Fix provider invoice generation validation * More QA fixes * Fix tests * QA defect resolutions * QA defect resolutions * Run dotnet format * Fix tests --------- Co-authored-by: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com>
This commit is contained in:
@@ -1,83 +0,0 @@
|
||||
using Bit.Admin.Billing.Models;
|
||||
using Bit.Admin.Enums;
|
||||
using Bit.Admin.Utilities;
|
||||
using Bit.Core.Billing.Providers.Migration.Models;
|
||||
using Bit.Core.Billing.Providers.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()
|
||||
: [];
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Admin.Billing.Models;
|
||||
|
||||
public class MigrateProvidersRequestModel
|
||||
{
|
||||
[Required]
|
||||
[Display(Name = "Provider IDs")]
|
||||
public string ProviderIds { get; set; }
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
@using System.Text.Json
|
||||
@model Bit.Core.Billing.Providers.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>
|
||||
@@ -1,46 +0,0 @@
|
||||
@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="mb-3">
|
||||
<label class="form-label" asp-for="ProviderIds"></label>
|
||||
<textarea rows="10" class="form-control" type="text" asp-for="ProviderIds"></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<input type="submit" value="Run" class="btn btn-primary"/>
|
||||
</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="mb-3">
|
||||
<label class="form-label" asp-for="ProviderIds"></label>
|
||||
<textarea rows="10" class="form-control" type="text" asp-for="ProviderIds"></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<input type="submit" value="See Previous Results" class="btn btn-primary"/>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
@@ -1,28 +0,0 @@
|
||||
@model Bit.Core.Billing.Providers.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>
|
||||
@@ -1,7 +1,6 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Bit.Admin.Enums;
|
||||
using Bit.Admin.Models;
|
||||
@@ -10,7 +9,6 @@ using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Organizations.Queries;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.BitStripe;
|
||||
using Bit.Core.Platform.Installations;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
@@ -33,7 +31,6 @@ public class ToolsController : Controller
|
||||
private readonly IInstallationRepository _installationRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IProviderUserRepository _providerUserRepository;
|
||||
private readonly IPaymentService _paymentService;
|
||||
private readonly IStripeAdapter _stripeAdapter;
|
||||
private readonly IWebHostEnvironment _environment;
|
||||
|
||||
@@ -46,7 +43,6 @@ public class ToolsController : Controller
|
||||
IInstallationRepository installationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IProviderUserRepository providerUserRepository,
|
||||
IPaymentService paymentService,
|
||||
IStripeAdapter stripeAdapter,
|
||||
IWebHostEnvironment environment)
|
||||
{
|
||||
@@ -58,7 +54,6 @@ public class ToolsController : Controller
|
||||
_installationRepository = installationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_providerUserRepository = providerUserRepository;
|
||||
_paymentService = paymentService;
|
||||
_stripeAdapter = stripeAdapter;
|
||||
_environment = environment;
|
||||
}
|
||||
@@ -341,138 +336,4 @@ public class ToolsController : Controller
|
||||
throw new Exception("No license to generate.");
|
||||
}
|
||||
}
|
||||
|
||||
[RequirePermission(Permission.Tools_ManageStripeSubscriptions)]
|
||||
public async Task<IActionResult> StripeSubscriptions(StripeSubscriptionListOptions options)
|
||||
{
|
||||
options = options ?? new StripeSubscriptionListOptions();
|
||||
options.Limit = 10;
|
||||
options.Expand = new List<string>() { "data.customer", "data.latest_invoice" };
|
||||
options.SelectAll = false;
|
||||
|
||||
var subscriptions = await _stripeAdapter.SubscriptionListAsync(options);
|
||||
|
||||
options.StartingAfter = subscriptions.LastOrDefault()?.Id;
|
||||
options.EndingBefore = await StripeSubscriptionsGetHasPreviousPage(subscriptions, options) ?
|
||||
subscriptions.FirstOrDefault()?.Id :
|
||||
null;
|
||||
|
||||
var isProduction = _environment.IsProduction();
|
||||
var model = new StripeSubscriptionsModel()
|
||||
{
|
||||
Items = subscriptions.Select(s => new StripeSubscriptionRowModel(s)).ToList(),
|
||||
Prices = (await _stripeAdapter.PriceListAsync(new Stripe.PriceListOptions() { Limit = 100 })).Data,
|
||||
TestClocks = isProduction ? new List<Stripe.TestHelpers.TestClock>() : await _stripeAdapter.TestClockListAsync(),
|
||||
Filter = options
|
||||
};
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[RequirePermission(Permission.Tools_ManageStripeSubscriptions)]
|
||||
public async Task<IActionResult> StripeSubscriptions([FromForm] StripeSubscriptionsModel model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
var isProduction = _environment.IsProduction();
|
||||
model.Prices = (await _stripeAdapter.PriceListAsync(new Stripe.PriceListOptions() { Limit = 100 })).Data;
|
||||
model.TestClocks = isProduction ? new List<Stripe.TestHelpers.TestClock>() : await _stripeAdapter.TestClockListAsync();
|
||||
return View(model);
|
||||
}
|
||||
|
||||
if (model.Action == StripeSubscriptionsAction.Export || model.Action == StripeSubscriptionsAction.BulkCancel)
|
||||
{
|
||||
var subscriptions = model.Filter.SelectAll ?
|
||||
await _stripeAdapter.SubscriptionListAsync(model.Filter) :
|
||||
model.Items.Where(x => x.Selected).Select(x => x.Subscription);
|
||||
|
||||
if (model.Action == StripeSubscriptionsAction.Export)
|
||||
{
|
||||
return StripeSubscriptionsExport(subscriptions);
|
||||
}
|
||||
|
||||
if (model.Action == StripeSubscriptionsAction.BulkCancel)
|
||||
{
|
||||
await StripeSubscriptionsCancel(subscriptions);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (model.Action == StripeSubscriptionsAction.PreviousPage || model.Action == StripeSubscriptionsAction.Search)
|
||||
{
|
||||
model.Filter.StartingAfter = null;
|
||||
}
|
||||
|
||||
if (model.Action == StripeSubscriptionsAction.NextPage || model.Action == StripeSubscriptionsAction.Search)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(model.Filter.StartingAfter))
|
||||
{
|
||||
var subscription = await _stripeAdapter.SubscriptionGetAsync(model.Filter.StartingAfter);
|
||||
if (subscription.Status == "canceled")
|
||||
{
|
||||
model.Filter.StartingAfter = null;
|
||||
}
|
||||
}
|
||||
model.Filter.EndingBefore = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return RedirectToAction("StripeSubscriptions", model.Filter);
|
||||
}
|
||||
|
||||
// This requires a redundant API call to Stripe because of the way they handle pagination.
|
||||
// The StartingBefore value has to be inferred from the list we get, and isn't supplied by Stripe.
|
||||
private async Task<bool> StripeSubscriptionsGetHasPreviousPage(List<Stripe.Subscription> subscriptions, StripeSubscriptionListOptions options)
|
||||
{
|
||||
var hasPreviousPage = false;
|
||||
if (subscriptions.FirstOrDefault()?.Id != null)
|
||||
{
|
||||
var previousPageSearchOptions = new StripeSubscriptionListOptions()
|
||||
{
|
||||
EndingBefore = subscriptions.FirstOrDefault().Id,
|
||||
Limit = 1,
|
||||
Status = options.Status,
|
||||
CurrentPeriodEndDate = options.CurrentPeriodEndDate,
|
||||
CurrentPeriodEndRange = options.CurrentPeriodEndRange,
|
||||
Price = options.Price
|
||||
};
|
||||
hasPreviousPage = (await _stripeAdapter.SubscriptionListAsync(previousPageSearchOptions)).Count > 0;
|
||||
}
|
||||
return hasPreviousPage;
|
||||
}
|
||||
|
||||
private async Task StripeSubscriptionsCancel(IEnumerable<Stripe.Subscription> subscriptions)
|
||||
{
|
||||
foreach (var s in subscriptions)
|
||||
{
|
||||
await _stripeAdapter.SubscriptionCancelAsync(s.Id);
|
||||
if (s.LatestInvoice?.Status == "open")
|
||||
{
|
||||
await _stripeAdapter.InvoiceVoidInvoiceAsync(s.LatestInvoiceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private FileResult StripeSubscriptionsExport(IEnumerable<Stripe.Subscription> subscriptions)
|
||||
{
|
||||
var fieldsToExport = subscriptions.Select(s => new
|
||||
{
|
||||
StripeId = s.Id,
|
||||
CustomerEmail = s.Customer?.Email,
|
||||
SubscriptionStatus = s.Status,
|
||||
InvoiceDueDate = s.CurrentPeriodEnd,
|
||||
SubscriptionProducts = s.Items?.Data.Select(p => p.Plan.Id)
|
||||
});
|
||||
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = true
|
||||
};
|
||||
|
||||
var result = System.Text.Json.JsonSerializer.Serialize(fieldsToExport, options);
|
||||
var bytes = Encoding.UTF8.GetBytes(result);
|
||||
return File(bytes, "application/json", "StripeSubscriptionsSearch.json");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,8 +52,6 @@ public enum Permission
|
||||
Tools_PromoteProviderServiceUser,
|
||||
Tools_GenerateLicenseFile,
|
||||
Tools_ManageTaxRates,
|
||||
Tools_ManageStripeSubscriptions,
|
||||
Tools_CreateEditTransaction,
|
||||
Tools_ProcessStripeEvents,
|
||||
Tools_MigrateProviders
|
||||
Tools_ProcessStripeEvents
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Models.BitStripe;
|
||||
|
||||
namespace Bit.Admin.Models;
|
||||
|
||||
public class StripeSubscriptionRowModel
|
||||
{
|
||||
public Stripe.Subscription Subscription { get; set; }
|
||||
public bool Selected { get; set; }
|
||||
|
||||
public StripeSubscriptionRowModel() { }
|
||||
public StripeSubscriptionRowModel(Stripe.Subscription subscription)
|
||||
{
|
||||
Subscription = subscription;
|
||||
}
|
||||
}
|
||||
|
||||
public enum StripeSubscriptionsAction
|
||||
{
|
||||
Search,
|
||||
PreviousPage,
|
||||
NextPage,
|
||||
Export,
|
||||
BulkCancel
|
||||
}
|
||||
|
||||
public class StripeSubscriptionsModel : IValidatableObject
|
||||
{
|
||||
public List<StripeSubscriptionRowModel> Items { get; set; }
|
||||
public StripeSubscriptionsAction Action { get; set; } = StripeSubscriptionsAction.Search;
|
||||
public string Message { get; set; }
|
||||
public List<Stripe.Price> Prices { get; set; }
|
||||
public List<Stripe.TestHelpers.TestClock> TestClocks { get; set; }
|
||||
public StripeSubscriptionListOptions Filter { get; set; } = new StripeSubscriptionListOptions();
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
if (Action == StripeSubscriptionsAction.BulkCancel && Filter.Status != "unpaid")
|
||||
{
|
||||
yield return new ValidationResult("Bulk cancel is currently only supported for unpaid subscriptions");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ using Microsoft.AspNetCore.Mvc.Razor;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Bit.Admin.Services;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Providers.Migration;
|
||||
|
||||
#if !OSS
|
||||
using Bit.Commercial.Core.Utilities;
|
||||
@@ -92,7 +91,6 @@ public class Startup
|
||||
services.AddDistributedCache(globalSettings);
|
||||
services.AddBillingOperations();
|
||||
services.AddHttpClient();
|
||||
services.AddProviderMigration();
|
||||
|
||||
#if OSS
|
||||
services.AddOosServices();
|
||||
|
||||
@@ -52,8 +52,7 @@ public static class RolePermissionMapping
|
||||
Permission.Tools_PromoteAdmin,
|
||||
Permission.Tools_PromoteProviderServiceUser,
|
||||
Permission.Tools_GenerateLicenseFile,
|
||||
Permission.Tools_ManageTaxRates,
|
||||
Permission.Tools_ManageStripeSubscriptions
|
||||
Permission.Tools_ManageTaxRates
|
||||
}
|
||||
},
|
||||
{ "admin", new List<Permission>
|
||||
@@ -105,7 +104,6 @@ public static class RolePermissionMapping
|
||||
Permission.Tools_PromoteProviderServiceUser,
|
||||
Permission.Tools_GenerateLicenseFile,
|
||||
Permission.Tools_ManageTaxRates,
|
||||
Permission.Tools_ManageStripeSubscriptions,
|
||||
Permission.Tools_CreateEditTransaction
|
||||
}
|
||||
},
|
||||
@@ -180,10 +178,8 @@ public static class RolePermissionMapping
|
||||
Permission.Tools_ChargeBrainTreeCustomer,
|
||||
Permission.Tools_GenerateLicenseFile,
|
||||
Permission.Tools_ManageTaxRates,
|
||||
Permission.Tools_ManageStripeSubscriptions,
|
||||
Permission.Tools_CreateEditTransaction,
|
||||
Permission.Tools_ProcessStripeEvents,
|
||||
Permission.Tools_MigrateProviders
|
||||
Permission.Tools_ProcessStripeEvents
|
||||
}
|
||||
},
|
||||
{ "sales", new List<Permission>
|
||||
|
||||
@@ -13,12 +13,10 @@
|
||||
var canPromoteAdmin = AccessControlService.UserHasPermission(Permission.Tools_PromoteAdmin);
|
||||
var canPromoteProviderServiceUser = AccessControlService.UserHasPermission(Permission.Tools_PromoteProviderServiceUser);
|
||||
var canGenerateLicense = AccessControlService.UserHasPermission(Permission.Tools_GenerateLicenseFile);
|
||||
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 || canPromoteProviderServiceUser ||
|
||||
canGenerateLicense || canManageStripeSubscriptions;
|
||||
canGenerateLicense;
|
||||
}
|
||||
|
||||
<!DOCTYPE html>
|
||||
@@ -102,12 +100,6 @@
|
||||
<a class="dropdown-item" asp-controller="Tools" asp-action="GenerateLicense">
|
||||
Generate License
|
||||
</a>
|
||||
}
|
||||
@if (canManageStripeSubscriptions)
|
||||
{
|
||||
<a class="dropdown-item" asp-controller="Tools" asp-action="StripeSubscriptions">
|
||||
Manage Stripe Subscriptions
|
||||
</a>
|
||||
}
|
||||
@if (canProcessStripeEvents)
|
||||
{
|
||||
@@ -115,12 +107,6 @@
|
||||
Process Stripe Events
|
||||
</a>
|
||||
}
|
||||
@if (canMigrateProviders)
|
||||
{
|
||||
<a class="dropdown-item" asp-controller="MigrateProviders" asp-action="index">
|
||||
Migrate Providers
|
||||
</a>
|
||||
}
|
||||
</ul>
|
||||
</li>
|
||||
}
|
||||
|
||||
@@ -1,277 +0,0 @@
|
||||
@model StripeSubscriptionsModel
|
||||
@{
|
||||
ViewData["Title"] = "Stripe Subscriptions";
|
||||
}
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
function onRowSelect(selectingPage = false) {
|
||||
let checkboxes = document.getElementsByClassName('row-check');
|
||||
let checkedCheckboxCount = 0;
|
||||
let bulkActions = document.getElementById('bulkActions');
|
||||
|
||||
let selectPage = document.getElementById('selectPage');
|
||||
for(let i = 0; i < checkboxes.length; i++){
|
||||
if((checkboxes[i].checked && !selectingPage) || selectingPage && selectPage.checked) {
|
||||
checkboxes[i].checked = true;
|
||||
checkedCheckboxCount += 1;
|
||||
} else {
|
||||
checkboxes[i].checked = false;
|
||||
}
|
||||
}
|
||||
|
||||
if(checkedCheckboxCount > 0) {
|
||||
bulkActions.classList.remove("d-none");
|
||||
} else {
|
||||
bulkActions.classList.add("d-none");
|
||||
}
|
||||
|
||||
let selectAll = document.getElementById('selectAll');
|
||||
if (checkedCheckboxCount === checkboxes.length) {
|
||||
selectPage.checked = true;
|
||||
selectAll.classList.remove("d-none");
|
||||
|
||||
let selectAllElement = document.getElementById('selectAllElement');
|
||||
selectAllElement.classList.remove('d-none');
|
||||
|
||||
let selectedAllConfirmation = document.getElementById('selectedAllConfirmation');
|
||||
selectedAllConfirmation.classList.add('d-none');
|
||||
} else {
|
||||
selectPage.checked = false;
|
||||
selectAll.classList.add("d-none");
|
||||
let selectAllInput = document.getElementById('selectAllInput');
|
||||
selectAllInput.checked = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onSelectAll() {
|
||||
let selectAllInput = document.getElementById('selectAllInput');
|
||||
selectAllInput.checked = true;
|
||||
|
||||
let selectAllElement = document.getElementById('selectAllElement');
|
||||
selectAllElement.classList.add('d-none');
|
||||
|
||||
let selectedAllConfirmation = document.getElementById('selectedAllConfirmation');
|
||||
selectedAllConfirmation.classList.remove('d-none');
|
||||
}
|
||||
|
||||
function exportSelectedSubscriptions() {
|
||||
let selectAll = document.getElementById('selectAll');
|
||||
let httpRequest = new XMLHttpRequest();
|
||||
httpRequest.open("POST");
|
||||
httpRequest.send();
|
||||
}
|
||||
|
||||
function cancelSelectedSubscriptions() {
|
||||
|
||||
}
|
||||
</script>
|
||||
}
|
||||
|
||||
<h2>Manage Stripe Subscriptions</h2>
|
||||
@if (!string.IsNullOrWhiteSpace(Model.Message))
|
||||
{
|
||||
<div class="alert alert-success"></div>
|
||||
}
|
||||
<form method="post">
|
||||
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" asp-for="Filter.Status">Status</label>
|
||||
<select asp-for="Filter.Status" name="filter.Status" class="form-select">
|
||||
<option asp-selected="Model.Filter.Status == null" value="all">All</option>
|
||||
<option asp-selected='Model.Filter.Status == "active"' value="active">Active</option>
|
||||
<option asp-selected='Model.Filter.Status == "unpaid"' value="unpaid">Unpaid</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" asp-for="Filter.CurrentPeriodEnd">Current Period End</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-text">
|
||||
<div class="form-check form-check-inline mb-0">
|
||||
<input type="radio" class="form-check-input" asp-for="Filter.CurrentPeriodEndRange" value="lt" id="beforeRadio">
|
||||
<label class="form-check-label me-2" for="beforeRadio">Before</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline mb-0">
|
||||
<input type="radio" class="form-check-input" asp-for="Filter.CurrentPeriodEndRange" value="gt" id="afterRadio">
|
||||
<label class="form-check-label" for="afterRadio">After</label>
|
||||
</div>
|
||||
</div>
|
||||
@{
|
||||
var date = @Model.Filter.CurrentPeriodEndDate.HasValue ? @Model.Filter.CurrentPeriodEndDate.Value.ToString("yyyy-MM-dd") : string.Empty;
|
||||
}
|
||||
<input type="date" class="form-control" asp-for="Filter.CurrentPeriodEndDate" name="filter.CurrentPeriodEndDate" value="@date">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" asp-for="Filter.Price">Price ID</label>
|
||||
<select asp-for="Filter.Price" name="filter.Price" class="form-select">
|
||||
<option asp-selected="Model.Filter.Price == null" value="@null">All</option>
|
||||
@foreach (var price in Model.Prices)
|
||||
{
|
||||
<option asp-selected='@(Model.Filter.Price == price.Id)' value="@price.Id">@price.Id</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" asp-for="Filter.TestClock">Test Clock</label>
|
||||
<select asp-for="Filter.TestClock" name="filter.TestClock" class="form-select">
|
||||
<option asp-selected="Model.Filter.TestClock == null" value="@null">All</option>
|
||||
@foreach (var clock in Model.TestClocks)
|
||||
{
|
||||
<option asp-selected='@(Model.Filter.TestClock == clock.Id)' value="@clock.Id">@clock.Name</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 text-end">
|
||||
<button type="submit" class="btn btn-primary" title="Search" name="action" asp-for="Action" value="@StripeSubscriptionsAction.Search">
|
||||
<i class="fa fa-search"></i> Search
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
<input type="checkbox" class="d-none" name="filter.SelectAll" id="selectAllInput" asp-for="@Model.Filter.SelectAll">
|
||||
<div class="text-center row d-flex justify-content-center">
|
||||
<div id="selectAll" class="d-none col-8">
|
||||
All @Model.Items.Count subscriptions on this page are selected.<br/>
|
||||
<button type="button" id="selectAllElement" class="btn btn-link p-0 pb-1" onclick="onSelectAll()">Click here to select all subscriptions for this search.</button>
|
||||
<span id="selectedAllConfirmation" class="d-none text-body-secondary">
|
||||
<i class="fa fa-check"></i> All subscriptions for this search are selected.
|
||||
</span>
|
||||
<div class="alert alert-warning mt-2" role="alert">
|
||||
Please be aware that bulk operations may take several minutes to complete.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<div class="form-check">
|
||||
<input id="selectPage" class="form-check-input" type="checkbox" onchange="onRowSelect(true)">
|
||||
</div>
|
||||
</th>
|
||||
<th>Id</th>
|
||||
<th>Customer Email</th>
|
||||
<th>Status</th>
|
||||
<th>Product Tier</th>
|
||||
<th>Current Period End</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if (!Model.Items.Any())
|
||||
{
|
||||
<tr>
|
||||
<td colspan="6">No results to list.</td>
|
||||
</tr>
|
||||
}
|
||||
else
|
||||
{
|
||||
@for (var i = 0; i < Model.Items.Count; i++)
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
@{
|
||||
var i0 = i;
|
||||
}
|
||||
<input type="hidden" asp-for="@Model.Items[i0].Subscription.Id" value="@Model.Items[i].Subscription.Id">
|
||||
<input type="hidden" asp-for="@Model.Items[i0].Subscription.Status" value="@Model.Items[i].Subscription.Status">
|
||||
<input type="hidden" asp-for="@Model.Items[i0].Subscription.CurrentPeriodEnd" value="@Model.Items[i].Subscription.CurrentPeriodEnd">
|
||||
<input type="hidden" asp-for="@Model.Items[i0].Subscription.Customer.Email" value="@Model.Items[i].Subscription.Customer.Email">
|
||||
<input type="hidden" asp-for="@Model.Items[i0].Subscription.LatestInvoice.Status" value="@Model.Items[i].Subscription.LatestInvoice.Status">
|
||||
<input type="hidden" asp-for="@Model.Items[i0].Subscription.LatestInvoice.Id" value="@Model.Items[i].Subscription.LatestInvoice.Id">
|
||||
|
||||
@for (var j = 0; j < Model.Items[i].Subscription.Items.Data.Count; j++)
|
||||
{
|
||||
var i1 = i;
|
||||
var j1 = j;
|
||||
<input
|
||||
type="hidden"
|
||||
asp-for="@Model.Items[i1].Subscription.Items.Data[j1].Plan.Id"
|
||||
value="@Model.Items[i].Subscription.Items.Data[j].Plan.Id">
|
||||
}
|
||||
<div class="form-check">
|
||||
|
||||
@{
|
||||
var i2 = i;
|
||||
}
|
||||
<input class="form-check-input row-check mt-0" onchange="onRowSelect()" asp-for="@Model.Items[i2].Selected">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
@Model.Items[i].Subscription.Id
|
||||
</td>
|
||||
<td>
|
||||
@Model.Items[i].Subscription.Customer?.Email
|
||||
</td>
|
||||
<td>
|
||||
@Model.Items[i].Subscription.Status
|
||||
</td>
|
||||
<td>
|
||||
@string.Join(",", Model.Items[i].Subscription.Items.Data.Select(product => product.Plan.Id).ToArray())
|
||||
</td>
|
||||
<td>
|
||||
@Model.Items[i].Subscription.CurrentPeriodEnd.ToShortDateString()
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<nav class="d-inline-flex align-items-center">
|
||||
<ul class="pagination mb-0">
|
||||
@if (!string.IsNullOrWhiteSpace(Model.Filter.EndingBefore))
|
||||
{
|
||||
<input type="hidden" asp-for="@Model.Filter.EndingBefore" value="@Model.Filter.EndingBefore">
|
||||
<li class="page-item">
|
||||
<button
|
||||
type="submit"
|
||||
class="page-link"
|
||||
name="action"
|
||||
asp-for="Action"
|
||||
value="@StripeSubscriptionsAction.PreviousPage">
|
||||
Previous
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
else
|
||||
{
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" tabindex="-1">Previous</a>
|
||||
</li>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(Model.Filter.StartingAfter))
|
||||
{
|
||||
<input type="hidden" asp-for="@Model.Filter.StartingAfter" value="@Model.Filter.StartingAfter">
|
||||
<li class="page-item">
|
||||
<button class="page-link"
|
||||
type="submit"
|
||||
name="action"
|
||||
asp-for="Action"
|
||||
value="@StripeSubscriptionsAction.NextPage">
|
||||
Next
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
else
|
||||
{
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" tabindex="-1">Next</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
<span id="bulkActions" class="d-none ms-3">
|
||||
<span class="d-inline-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary" name="action" asp-for="Action" value="@StripeSubscriptionsAction.Export">
|
||||
Export
|
||||
</button>
|
||||
<button type="submit" class="btn btn-danger" name="action" asp-for="Action" value="@StripeSubscriptionsAction.BulkCancel">
|
||||
Bulk Cancel
|
||||
</button>
|
||||
</span>
|
||||
</span>
|
||||
</nav>
|
||||
</form>
|
||||
Reference in New Issue
Block a user