mirror of
https://github.com/bitwarden/server
synced 2026-02-20 19:33:32 +00:00
[PM-30108] import discount from stripe (#6982)
* [PM-30108] import discount from stripe * fix repo tests * pr feedback * wrap discounts in feature flag * claude pr feedback
This commit is contained in:
209
src/Admin/Billing/Controllers/SubscriptionDiscountsController.cs
Normal file
209
src/Admin/Billing/Controllers/SubscriptionDiscountsController.cs
Normal file
@@ -0,0 +1,209 @@
|
||||
using Bit.Admin.Billing.Models;
|
||||
using Bit.Admin.Enums;
|
||||
using Bit.Admin.Utilities;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Subscriptions.Entities;
|
||||
using Bit.Core.Billing.Subscriptions.Repositories;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Admin.Billing.Controllers;
|
||||
|
||||
[Authorize]
|
||||
[Route("subscription-discounts")]
|
||||
public class SubscriptionDiscountsController(
|
||||
ISubscriptionDiscountRepository subscriptionDiscountRepository,
|
||||
IStripeAdapter stripeAdapter,
|
||||
ILogger<SubscriptionDiscountsController> logger) : Controller
|
||||
{
|
||||
private const string SuccessKey = "Success";
|
||||
|
||||
[HttpGet]
|
||||
[RequirePermission(Permission.Tools_CreateEditTransaction)]
|
||||
public async Task<IActionResult> Index(int page = 1, int count = 25)
|
||||
{
|
||||
if (page < 1)
|
||||
{
|
||||
page = 1;
|
||||
}
|
||||
|
||||
if (count < 1)
|
||||
{
|
||||
count = 1;
|
||||
}
|
||||
|
||||
var skip = (page - 1) * count;
|
||||
var discounts = await subscriptionDiscountRepository.ListAsync(skip, count);
|
||||
|
||||
var discountViewModels = discounts.Select(d => new SubscriptionDiscountViewModel
|
||||
{
|
||||
Id = d.Id,
|
||||
StripeCouponId = d.StripeCouponId,
|
||||
Name = d.Name,
|
||||
PercentOff = d.PercentOff,
|
||||
AmountOff = d.AmountOff,
|
||||
Currency = d.Currency,
|
||||
Duration = d.Duration,
|
||||
DurationInMonths = d.DurationInMonths,
|
||||
StartDate = d.StartDate,
|
||||
EndDate = d.EndDate,
|
||||
AudienceType = d.AudienceType,
|
||||
CreationDate = d.CreationDate
|
||||
}).ToList();
|
||||
|
||||
var model = new SubscriptionDiscountPagedModel
|
||||
{
|
||||
Items = discountViewModels,
|
||||
Page = page,
|
||||
Count = count
|
||||
};
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpGet("create")]
|
||||
[RequirePermission(Permission.Tools_CreateEditTransaction)]
|
||||
public IActionResult Create()
|
||||
{
|
||||
return View(new CreateSubscriptionDiscountModel());
|
||||
}
|
||||
|
||||
[HttpPost("import-coupon")]
|
||||
[ValidateAntiForgeryToken]
|
||||
[RequirePermission(Permission.Tools_CreateEditTransaction)]
|
||||
public async Task<IActionResult> ImportCoupon(CreateSubscriptionDiscountModel model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View("Create", model);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var existing = await subscriptionDiscountRepository.GetByStripeCouponIdAsync(model.StripeCouponId);
|
||||
if (existing != null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.StripeCouponId),
|
||||
"This coupon has already been imported.");
|
||||
return View("Create", model);
|
||||
}
|
||||
|
||||
Coupon coupon;
|
||||
try
|
||||
{
|
||||
var options = new CouponGetOptions();
|
||||
options.AddExpand(StripeConstants.CouponExpandablePropertyNames.AppliesTo);
|
||||
coupon = await stripeAdapter.GetCouponAsync(model.StripeCouponId, options);
|
||||
}
|
||||
catch (StripeException ex)
|
||||
{
|
||||
var errorMessage = ex.StripeError?.Code == "resource_missing"
|
||||
? "Coupon not found in Stripe. Please verify the coupon ID."
|
||||
: "An error occurred while fetching the coupon from Stripe.";
|
||||
|
||||
logger.LogError(ex, "Stripe coupon error: {CouponId}", model.StripeCouponId);
|
||||
ModelState.AddModelError(nameof(model.StripeCouponId), errorMessage);
|
||||
return View("Create", model);
|
||||
}
|
||||
|
||||
model.Name = coupon.Name;
|
||||
model.PercentOff = coupon.PercentOff;
|
||||
model.AmountOff = coupon.AmountOff;
|
||||
model.Currency = coupon.Currency;
|
||||
model.Duration = coupon.Duration;
|
||||
model.DurationInMonths = (int?)coupon.DurationInMonths;
|
||||
|
||||
var productIds = coupon.AppliesTo?.Products;
|
||||
if (productIds != null && productIds.Count != 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
var allProducts = await stripeAdapter.ListProductsAsync(new ProductListOptions
|
||||
{
|
||||
Ids = productIds.ToList()
|
||||
});
|
||||
|
||||
model.AppliesToProducts = allProducts
|
||||
.ToDictionary(product => product.Id, product => product.Name);
|
||||
}
|
||||
catch (StripeException ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to fetch the coupon's associated products from Stripe. Coupon ID: {CouponId}", model.StripeCouponId);
|
||||
ModelState.AddModelError(string.Empty, "Failed to fetch the coupon's associated products from Stripe.");
|
||||
return View("Create", model);
|
||||
}
|
||||
}
|
||||
|
||||
model.IsImported = true;
|
||||
return View("Create", model);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error importing coupon from Stripe. Coupon ID: {CouponId}", model.StripeCouponId);
|
||||
ModelState.AddModelError(string.Empty, "An error occurred while importing the coupon.");
|
||||
return View("Create", model);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("create")]
|
||||
[ValidateAntiForgeryToken]
|
||||
[RequirePermission(Permission.Tools_CreateEditTransaction)]
|
||||
public async Task<IActionResult> Create(CreateSubscriptionDiscountModel model)
|
||||
{
|
||||
if (!model.IsImported)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty,
|
||||
"Please import the coupon from Stripe before submitting.");
|
||||
return View(model);
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Check for duplicate coupon to prevent race condition
|
||||
var existing = await subscriptionDiscountRepository.GetByStripeCouponIdAsync(model.StripeCouponId);
|
||||
if (existing != null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.StripeCouponId),
|
||||
"This coupon has already been imported.");
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var discount = new SubscriptionDiscount
|
||||
{
|
||||
StripeCouponId = model.StripeCouponId,
|
||||
Name = model.Name,
|
||||
PercentOff = model.PercentOff,
|
||||
AmountOff = model.AmountOff,
|
||||
Currency = model.Currency,
|
||||
Duration = model.Duration,
|
||||
DurationInMonths = model.DurationInMonths,
|
||||
StripeProductIds = model.AppliesToProducts?.Keys.ToList(),
|
||||
StartDate = model.StartDate,
|
||||
EndDate = model.EndDate,
|
||||
AudienceType = model.AudienceType,
|
||||
CreationDate = DateTime.UtcNow,
|
||||
RevisionDate = DateTime.UtcNow
|
||||
};
|
||||
|
||||
await subscriptionDiscountRepository.CreateAsync(discount);
|
||||
|
||||
PersistSuccessMessage($"Discount '{model.StripeCouponId}' imported successfully.");
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error creating subscription discount. Coupon ID: {CouponId}", model.StripeCouponId);
|
||||
ModelState.AddModelError(string.Empty, "An error occurred while saving the discount.");
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
|
||||
private void PersistSuccessMessage(string message) => TempData[SuccessKey] = message;
|
||||
}
|
||||
47
src/Admin/Billing/Models/CreateSubscriptionDiscountModel.cs
Normal file
47
src/Admin/Billing/Models/CreateSubscriptionDiscountModel.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Billing.Enums;
|
||||
|
||||
namespace Bit.Admin.Billing.Models;
|
||||
|
||||
public class CreateSubscriptionDiscountModel : IValidatableObject
|
||||
{
|
||||
[Required]
|
||||
[Display(Name = "Stripe Coupon ID")]
|
||||
[MaxLength(50)]
|
||||
public string StripeCouponId { get; set; } = null!;
|
||||
|
||||
public string? Name { get; set; }
|
||||
public decimal? PercentOff { get; set; }
|
||||
public long? AmountOff { get; set; }
|
||||
public string? Currency { get; set; }
|
||||
public string Duration { get; set; } = string.Empty;
|
||||
public int? DurationInMonths { get; set; }
|
||||
public Dictionary<string, string>? AppliesToProducts { get; set; } // Key: ProductId, Value: ProductName
|
||||
|
||||
[Required]
|
||||
[Display(Name = "Start Date")]
|
||||
public DateTime StartDate { get; set; } = DateTime.UtcNow.Date;
|
||||
|
||||
[Required]
|
||||
[Display(Name = "End Date")]
|
||||
public DateTime EndDate { get; set; } = DateTime.UtcNow.Date.AddMonths(1);
|
||||
|
||||
[Display(Name = "Restrict to users with no previous subscriptions?")]
|
||||
public bool RestrictToNewUsersOnly { get; set; }
|
||||
|
||||
public DiscountAudienceType AudienceType => RestrictToNewUsersOnly
|
||||
? DiscountAudienceType.UserHasNoPreviousSubscriptions
|
||||
: DiscountAudienceType.AllUsers;
|
||||
|
||||
public bool IsImported { get; set; }
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
if (EndDate < StartDate)
|
||||
{
|
||||
yield return new ValidationResult(
|
||||
"End Date must be on or after Start Date.",
|
||||
new[] { nameof(EndDate) });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using Bit.Admin.Models;
|
||||
|
||||
namespace Bit.Admin.Billing.Models;
|
||||
|
||||
public class SubscriptionDiscountPagedModel : PagedModel<SubscriptionDiscountViewModel>
|
||||
{
|
||||
}
|
||||
27
src/Admin/Billing/Models/SubscriptionDiscountViewModel.cs
Normal file
27
src/Admin/Billing/Models/SubscriptionDiscountViewModel.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
|
||||
namespace Bit.Admin.Billing.Models;
|
||||
|
||||
public class SubscriptionDiscountViewModel
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string StripeCouponId { get; set; } = null!;
|
||||
public string? Name { get; set; }
|
||||
public decimal? PercentOff { get; set; }
|
||||
public long? AmountOff { get; set; }
|
||||
public string? Currency { get; set; }
|
||||
public string Duration { get; set; } = null!;
|
||||
public int? DurationInMonths { get; set; }
|
||||
public DateTime StartDate { get; set; }
|
||||
public DateTime EndDate { get; set; }
|
||||
public DiscountAudienceType AudienceType { get; set; }
|
||||
public DateTime CreationDate { get; set; }
|
||||
public bool IsActive => DateTime.UtcNow >= StartDate && DateTime.UtcNow <= EndDate;
|
||||
|
||||
public string DiscountDisplay => PercentOff.HasValue
|
||||
? $"{PercentOff.Value:G29}% off"
|
||||
: $"${AmountOff / 100m} off";
|
||||
|
||||
public bool IsRestrictedToNewUsersOnly => AudienceType == DiscountAudienceType.UserHasNoPreviousSubscriptions;
|
||||
public bool IsAvailableToAllUsers => AudienceType == DiscountAudienceType.AllUsers;
|
||||
}
|
||||
17
src/Admin/Billing/Views/SubscriptionDiscounts/Create.cshtml
Normal file
17
src/Admin/Billing/Views/SubscriptionDiscounts/Create.cshtml
Normal file
@@ -0,0 +1,17 @@
|
||||
@model CreateSubscriptionDiscountModel
|
||||
@{
|
||||
ViewData["Title"] = "Import Discount From Stripe";
|
||||
}
|
||||
|
||||
<h1>Import Discount From Stripe</h1>
|
||||
|
||||
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
||||
|
||||
@if (!Model.IsImported)
|
||||
{
|
||||
<partial name="Partials/_ImportCouponForm" model="Model" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<partial name="Partials/_ConfigureDiscountForm" model="Model" />
|
||||
}
|
||||
127
src/Admin/Billing/Views/SubscriptionDiscounts/Index.cshtml
Normal file
127
src/Admin/Billing/Views/SubscriptionDiscounts/Index.cshtml
Normal file
@@ -0,0 +1,127 @@
|
||||
@model SubscriptionDiscountPagedModel
|
||||
@{
|
||||
ViewData["Title"] = "Discounts";
|
||||
}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<h1>Discounts</h1>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a asp-action="Create" class="btn btn-primary">
|
||||
<i class="fa fa-plus"></i> Import Discount
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (Model.Items.Any())
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Stripe Coupon ID</th>
|
||||
<th>Name</th>
|
||||
<th>Discount</th>
|
||||
<th>Duration</th>
|
||||
<th>Only New Users?</th>
|
||||
<th>Status</th>
|
||||
<th>Created</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var discount in Model.Items)
|
||||
{
|
||||
var dateRange = $"{discount.StartDate.ToString("MM/dd/yyyy")} - {discount.EndDate.ToString("MM/dd/yyyy")}";
|
||||
<tr>
|
||||
<td><code>@discount.StripeCouponId</code></td>
|
||||
<td>
|
||||
<a asp-action="Edit" asp-route-id="@discount.Id">
|
||||
@discount.Name
|
||||
</a>
|
||||
</td>
|
||||
<td>@discount.DiscountDisplay</td>
|
||||
<td>
|
||||
@discount.Duration
|
||||
@if (discount.DurationInMonths.HasValue)
|
||||
{
|
||||
<text>(@discount.DurationInMonths months)</text>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (discount.IsRestrictedToNewUsersOnly)
|
||||
{
|
||||
<i class="fa fa-check"></i>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (discount.IsActive)
|
||||
{
|
||||
<span class="badge bg-success" title="@dateRange">Active</span>
|
||||
}
|
||||
else if (DateTime.UtcNow < discount.StartDate)
|
||||
{
|
||||
<span class="badge bg-secondary" title="@dateRange">Scheduled</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-danger" title="@dateRange">Expired</span>
|
||||
}
|
||||
</td>
|
||||
<td>@discount.CreationDate.ToString("MM/dd/yyyy")</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<nav>
|
||||
<ul class="pagination">
|
||||
@if (Model.PreviousPage.HasValue)
|
||||
{
|
||||
<li class="page-item">
|
||||
<a class="page-link" asp-action="Index"
|
||||
asp-route-page="@Model.PreviousPage.Value"
|
||||
asp-route-count="@Model.Count">
|
||||
Previous
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
else
|
||||
{
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" tabindex="-1">Previous</a>
|
||||
</li>
|
||||
}
|
||||
|
||||
<li class="page-item active">
|
||||
<span class="page-link">Page @Model.Page</span>
|
||||
</li>
|
||||
|
||||
@if (Model.NextPage.HasValue)
|
||||
{
|
||||
<li class="page-item">
|
||||
<a class="page-link" asp-action="Index"
|
||||
asp-route-page="@Model.NextPage.Value"
|
||||
asp-route-count="@Model.Count">
|
||||
Next
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
else
|
||||
{
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" tabindex="-1">Next</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="alert alert-info">
|
||||
No subscription discounts found. <a asp-action="Create">Import one from Stripe</a>.
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,115 @@
|
||||
@model CreateSubscriptionDiscountModel
|
||||
|
||||
<form method="post" asp-action="Create">
|
||||
<input type="hidden" asp-for="StripeCouponId" />
|
||||
<input type="hidden" asp-for="Name" />
|
||||
<input type="hidden" asp-for="PercentOff" />
|
||||
<input type="hidden" asp-for="AmountOff" />
|
||||
<input type="hidden" asp-for="Currency" />
|
||||
<input type="hidden" asp-for="Duration" />
|
||||
<input type="hidden" asp-for="DurationInMonths" />
|
||||
<input type="hidden" asp-for="IsImported" />
|
||||
@if (Model.AppliesToProducts != null)
|
||||
{
|
||||
var index = 0;
|
||||
@foreach (var product in Model.AppliesToProducts)
|
||||
{
|
||||
<input type="hidden" name="AppliesToProducts[@index].Key" value="@product.Key" />
|
||||
<input type="hidden" name="AppliesToProducts[@index].Value" value="@product.Value" />
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5>Stripe Coupon Details</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-3">Coupon ID:</dt>
|
||||
<dd class="col-sm-9"><code>@Model.StripeCouponId</code></dd>
|
||||
|
||||
@if (!string.IsNullOrEmpty(Model.Name))
|
||||
{
|
||||
<dt class="col-sm-3">Name:</dt>
|
||||
<dd class="col-sm-9">@Model.Name</dd>
|
||||
}
|
||||
|
||||
<dt class="col-sm-3">Discount:</dt>
|
||||
<dd class="col-sm-9">
|
||||
@if (Model.PercentOff.HasValue)
|
||||
{
|
||||
<text>@Model.PercentOff% off</text>
|
||||
}
|
||||
else if (Model.AmountOff.HasValue)
|
||||
{
|
||||
<text>$@(Model.AmountOff / 100m) off</text>
|
||||
@if (!string.IsNullOrEmpty(Model.Currency))
|
||||
{
|
||||
<text> (@Model.Currency.ToUpper())</text>
|
||||
}
|
||||
}
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-3">Duration:</dt>
|
||||
<dd class="col-sm-9">
|
||||
@Model.Duration
|
||||
@if (Model.DurationInMonths.HasValue)
|
||||
{
|
||||
<text>(@Model.DurationInMonths months)</text>
|
||||
}
|
||||
</dd>
|
||||
|
||||
@if (Model.AppliesToProducts != null && Model.AppliesToProducts.Count != 0)
|
||||
{
|
||||
<dt class="col-sm-3">Applies To Products:</dt>
|
||||
<dd class="col-sm-9">
|
||||
<ul class="mb-0">
|
||||
@foreach (var product in Model.AppliesToProducts)
|
||||
{
|
||||
<li>@product.Value</li>
|
||||
}
|
||||
</ul>
|
||||
</dd>
|
||||
}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5>Step 2: Bitwarden Configuration</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label asp-for="StartDate" class="form-label"></label>
|
||||
<input asp-for="StartDate" type="date" class="form-control" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label asp-for="EndDate" class="form-label"></label>
|
||||
<input asp-for="EndDate" type="date" class="form-control" required />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 form-check">
|
||||
<input asp-for="RestrictToNewUsersOnly" type="checkbox" class="form-check-input" />
|
||||
<label asp-for="RestrictToNewUsersOnly" class="form-check-label"></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="fa fa-save"></i> Save Discount
|
||||
</button>
|
||||
<a asp-action="Create" class="btn btn-secondary">
|
||||
Back
|
||||
</a>
|
||||
<a asp-action="Index" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,25 @@
|
||||
@model CreateSubscriptionDiscountModel
|
||||
|
||||
<form method="post" asp-action="ImportCoupon">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5>Step 1: Import Stripe Coupon</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label asp-for="StripeCouponId" class="form-label"></label>
|
||||
<input asp-for="StripeCouponId" class="form-control" autofocus />
|
||||
<small class="form-text text-muted">
|
||||
Enter the Stripe coupon ID and click Import to fetch the coupon details from Stripe.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fa fa-download"></i> Import from Stripe
|
||||
</button>
|
||||
<a asp-action="Index" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,5 +1,7 @@
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
@using Bit.Admin.AdminConsole
|
||||
@using Bit.Admin.AdminConsole.Models
|
||||
@using Bit.Admin.Billing.Models
|
||||
@using Bit.Core.Billing.Enums
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@addTagHelper "*, Admin"
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
@using Bit.Admin.Enums;
|
||||
@using Bit.Core;
|
||||
|
||||
@inject SignInManager<IdentityUser> SignInManager
|
||||
@inject Bit.Core.Settings.GlobalSettings GlobalSettings
|
||||
@inject Bit.Admin.Services.IAccessControlService AccessControlService
|
||||
@inject Bit.Core.Services.IFeatureService FeatureService
|
||||
|
||||
@{
|
||||
var canViewUsers = AccessControlService.UserHasPermission(Permission.User_List_View);
|
||||
@@ -14,6 +16,7 @@
|
||||
var canPromoteProviderServiceUser = AccessControlService.UserHasPermission(Permission.Tools_PromoteProviderServiceUser);
|
||||
var canGenerateLicense = AccessControlService.UserHasPermission(Permission.Tools_GenerateLicenseFile);
|
||||
var canProcessStripeEvents = AccessControlService.UserHasPermission(Permission.Tools_ProcessStripeEvents);
|
||||
var enablePersonalDiscounts = FeatureService.IsEnabled(FeatureFlagKeys.PM29108_EnablePersonalDiscounts);
|
||||
|
||||
var canViewTools = canChargeBraintree || canCreateTransaction || canPromoteAdmin || canPromoteProviderServiceUser ||
|
||||
canGenerateLicense;
|
||||
@@ -63,6 +66,14 @@
|
||||
<a class="nav-link" asp-controller="Providers" asp-action="Index">Providers</a>
|
||||
</li>
|
||||
}
|
||||
@if (canCreateTransaction && enablePersonalDiscounts)
|
||||
{
|
||||
<li class="nav-item" active-controller="SubscriptionDiscounts">
|
||||
<a class="nav-link" asp-controller="SubscriptionDiscounts" asp-action="Index">
|
||||
Discounts
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
@if (canViewTools)
|
||||
{
|
||||
<li class="nav-item dropdown" active-controller="tools">
|
||||
|
||||
Reference in New Issue
Block a user