From 6b7ef6eb4f9c4a7a3bfd3d61b8a4e4798d4a36a1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 31 May 2026 17:46:07 +0000
Subject: [PATCH 01/16] Add discount management to Grand.Web.Store
(store-scoped CRUD)
Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com>
---
.../Store/Views/Discount/BrandAddPopup.cshtml | 168 ++++
.../Views/Discount/CategoryAddPopup.cshtml | 175 ++++
.../Views/Discount/CollectionAddPopup.cshtml | 175 ++++
.../Areas/Store/Views/Discount/Create.cshtml | 35 +
.../Areas/Store/Views/Discount/Edit.cshtml | 39 +
.../Areas/Store/Views/Discount/List.cshtml | 169 ++++
.../CreateOrUpdate.TabAppliedToBrands.cshtml | 107 +++
...eateOrUpdate.TabAppliedToCategories.cshtml | 107 +++
...ateOrUpdate.TabAppliedToCollections.cshtml | 106 +++
...CreateOrUpdate.TabAppliedToProducts.cshtml | 109 +++
.../CreateOrUpdate.TabCouponCodes.cshtml | 99 +++
.../Partials/CreateOrUpdate.TabHistory.cshtml | 75 ++
.../Partials/CreateOrUpdate.TabInfo.cshtml | 260 ++++++
.../CreateOrUpdate.TabRequirements.cshtml | 207 +++++
.../Discount/Partials/CreateOrUpdate.cshtml | 67 ++
.../Views/Discount/ProductAddPopup.cshtml | 174 ++++
.../Areas/Store/Views/_ViewImports.cshtml | 1 +
.../Controllers/DiscountController.cs | 783 ++++++++++++++++++
18 files changed, 2856 insertions(+)
create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/Discount/BrandAddPopup.cshtml
create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/Discount/CategoryAddPopup.cshtml
create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/Discount/CollectionAddPopup.cshtml
create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Create.cshtml
create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Edit.cshtml
create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/Discount/List.cshtml
create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.TabAppliedToBrands.cshtml
create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.TabAppliedToCategories.cshtml
create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.TabAppliedToCollections.cshtml
create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.TabAppliedToProducts.cshtml
create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.TabCouponCodes.cshtml
create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.TabHistory.cshtml
create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.TabInfo.cshtml
create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.TabRequirements.cshtml
create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.cshtml
create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/Discount/ProductAddPopup.cshtml
create mode 100644 src/Web/Grand.Web.Store/Controllers/DiscountController.cs
diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/BrandAddPopup.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/BrandAddPopup.cshtml
new file mode 100644
index 000000000..0bd06081d
--- /dev/null
+++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/BrandAddPopup.cshtml
@@ -0,0 +1,168 @@
+@model DiscountModel.AddBrandToDiscountModel
+@inject AdminAreaSettings adminAreaSettings
+@{
+ Layout = "";
+ ViewBag.Title = Loc["admin.marketing.Discounts.AppliedToBrands.AddNew"];
+}
+
+
diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/CategoryAddPopup.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/CategoryAddPopup.cshtml
new file mode 100644
index 000000000..bb6502f1f
--- /dev/null
+++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/CategoryAddPopup.cshtml
@@ -0,0 +1,175 @@
+@model DiscountModel.AddCategoryToDiscountModel
+@inject AdminAreaSettings adminAreaSettings
+@{
+ Layout = "";
+ ViewBag.Title = Loc["admin.marketing.Discounts.AppliedToCategories.AddNew"];
+}
+
+
diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/CollectionAddPopup.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/CollectionAddPopup.cshtml
new file mode 100644
index 000000000..0dac9032c
--- /dev/null
+++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/CollectionAddPopup.cshtml
@@ -0,0 +1,175 @@
+@model DiscountModel.AddCollectionToDiscountModel
+@inject AdminAreaSettings adminAreaSettings
+@{
+ Layout = "";
+ ViewBag.Title = Loc["admin.marketing.Discounts.AppliedToCollections.AddNew"];
+}
+
+
diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Create.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Create.cshtml
new file mode 100644
index 000000000..3ef7f976a
--- /dev/null
+++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Create.cshtml
@@ -0,0 +1,35 @@
+@model DiscountModel
+@{
+ ViewBag.Title = Loc["admin.marketing.Discounts.AddNew"];
+}
+
diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Edit.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Edit.cshtml
new file mode 100644
index 000000000..3d440744f
--- /dev/null
+++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Edit.cshtml
@@ -0,0 +1,39 @@
+@model DiscountModel
+@{
+ ViewBag.Title = Loc["admin.marketing.Discounts.EditDiscountDetails"];
+}
+
+
diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/List.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/List.cshtml
new file mode 100644
index 000000000..3920812d7
--- /dev/null
+++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/List.cshtml
@@ -0,0 +1,169 @@
+@model DiscountListModel
+@inject AdminAreaSettings adminAreaSettings
+@{
+ ViewBag.Title = Loc["admin.marketing.Discounts"];
+}
+
+
+
+
+
diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.TabAppliedToBrands.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.TabAppliedToBrands.cshtml
new file mode 100644
index 000000000..b2df80bff
--- /dev/null
+++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.TabAppliedToBrands.cshtml
@@ -0,0 +1,107 @@
+@model DiscountModel
+@{
+ if (!string.IsNullOrEmpty(Model.Id))
+ {
+
+
+
+
+ }
+ else
+ {
+
+ @Loc["admin.marketing.Discounts.AppliedToBrands.SaveBeforeEdit"]
+
+ }
+}
diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.TabAppliedToCategories.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.TabAppliedToCategories.cshtml
new file mode 100644
index 000000000..569fc171a
--- /dev/null
+++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.TabAppliedToCategories.cshtml
@@ -0,0 +1,107 @@
+@model DiscountModel
+@{
+ if (!string.IsNullOrEmpty(Model.Id))
+ {
+
+
+
+
+ }
+ else
+ {
+
+ @Loc["admin.marketing.Discounts.AppliedToCategories.SaveBeforeEdit"]
+
+ }
+}
diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.TabAppliedToCollections.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.TabAppliedToCollections.cshtml
new file mode 100644
index 000000000..c7b638d8f
--- /dev/null
+++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.TabAppliedToCollections.cshtml
@@ -0,0 +1,106 @@
+@model DiscountModel
+@{
+ if (!string.IsNullOrEmpty(Model.Id))
+ {
+
+
+
+ }
+ else
+ {
+
+ @Loc["admin.marketing.Discounts.AppliedToCollections.SaveBeforeEdit"]
+
+ }
+}
diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.TabAppliedToProducts.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.TabAppliedToProducts.cshtml
new file mode 100644
index 000000000..ca5710b26
--- /dev/null
+++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.TabAppliedToProducts.cshtml
@@ -0,0 +1,109 @@
+@model DiscountModel
+@inject AdminAreaSettings adminAreaSettings
+
+@{
+ if (!string.IsNullOrEmpty(Model.Id))
+ {
+
+
+
+
+
+ }
+ else
+ {
+
+ @Loc["admin.marketing.Discounts.AppliedToProducts.SaveBeforeEdit"]
+
+ }
+}
diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.TabCouponCodes.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.TabCouponCodes.cshtml
new file mode 100644
index 000000000..cc95bdf4c
--- /dev/null
+++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.TabCouponCodes.cshtml
@@ -0,0 +1,99 @@
+@model DiscountModel
+@inject AdminAreaSettings adminAreaSettings
+
+@if (!string.IsNullOrEmpty(Model.Id))
+{
+
+
+}
+else
+{
+
+ @Loc["admin.marketing.Discounts.CouponCodes.SaveBeforeEdit"]
+
+}
diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.TabHistory.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.TabHistory.cshtml
new file mode 100644
index 000000000..ad2b4659f
--- /dev/null
+++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.TabHistory.cshtml
@@ -0,0 +1,75 @@
+@model DiscountModel
+@inject AdminAreaSettings adminAreaSettings
+@{
+
+
+}
diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.TabInfo.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.TabInfo.cshtml
new file mode 100644
index 000000000..14046cf5e
--- /dev/null
+++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.TabInfo.cshtml
@@ -0,0 +1,260 @@
+@using Grand.Domain.Discounts
+@model DiscountModel
+
+
+
diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.TabRequirements.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.TabRequirements.cshtml
new file mode 100644
index 000000000..fe615efa4
--- /dev/null
+++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.TabRequirements.cshtml
@@ -0,0 +1,207 @@
+@model DiscountModel
+
+@{
+ if (!string.IsNullOrEmpty(Model.Id))
+ {
+
+
+
+
+
+
+
+
+
+
+ }
+ else
+ {
+
+ @Loc["admin.marketing.Discounts.Requirements.SaveBeforeEdit"]
+
+ }
+}
diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.cshtml
new file mode 100644
index 000000000..78e3f9b96
--- /dev/null
+++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.cshtml
@@ -0,0 +1,67 @@
+@model DiscountModel
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @if (!string.IsNullOrEmpty(Model.Id))
+ {
+
+
+
+
+
+ }
+
+
diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/ProductAddPopup.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/ProductAddPopup.cshtml
new file mode 100644
index 000000000..174392a8d
--- /dev/null
+++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/ProductAddPopup.cshtml
@@ -0,0 +1,174 @@
+@model DiscountModel.AddProductToDiscountModel
+@inject AdminAreaSettings adminAreaSettings
+@{
+ Layout = "";
+ ViewBag.Title = Loc["admin.marketing.Discounts.AppliedToProducts.AddNew"];
+}
+
+
diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/_ViewImports.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/_ViewImports.cshtml
index 1420c6933..d330949c8 100644
--- a/src/Web/Grand.Web.Store/Areas/Store/Views/_ViewImports.cshtml
+++ b/src/Web/Grand.Web.Store/Areas/Store/Views/_ViewImports.cshtml
@@ -35,6 +35,7 @@
@using Grand.Web.AdminShared.Models.Pages
@using Grand.Web.AdminShared.Models.Settings
@using Grand.Web.AdminShared.Models.Shipping
+@using Grand.Web.AdminShared.Models.Discounts
@inject LocService Loc
@inject IEnumTranslationService EnumTranslationService
\ No newline at end of file
diff --git a/src/Web/Grand.Web.Store/Controllers/DiscountController.cs b/src/Web/Grand.Web.Store/Controllers/DiscountController.cs
new file mode 100644
index 000000000..f2294ba7f
--- /dev/null
+++ b/src/Web/Grand.Web.Store/Controllers/DiscountController.cs
@@ -0,0 +1,783 @@
+using Grand.Business.Core.Interfaces.Catalog.Brands;
+using Grand.Business.Core.Interfaces.Catalog.Categories;
+using Grand.Business.Core.Interfaces.Catalog.Collections;
+using Grand.Business.Core.Interfaces.Catalog.Discounts;
+using Grand.Business.Core.Interfaces.Catalog.Products;
+using Grand.Business.Core.Interfaces.Common.Directory;
+using Grand.Business.Core.Interfaces.Common.Localization;
+using Grand.Business.Core.Queries.Catalog;
+using Grand.Domain.Permissions;
+using Grand.Infrastructure;
+using Grand.Web.AdminShared.Extensions;
+using Grand.Web.AdminShared.Extensions.Mapping;
+using Grand.Web.AdminShared.Interfaces;
+using Grand.Web.AdminShared.Models.Catalog;
+using Grand.Web.AdminShared.Models.Discounts;
+using Grand.Web.Common.DataSource;
+using Grand.Web.Common.Filters;
+using Grand.Web.Common.Security.Authorization;
+using MediatR;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Grand.Web.Store.Controllers;
+
+[PermissionAuthorize(PermissionSystemName.Discounts)]
+public class DiscountController : BaseStoreController
+{
+ #region Constructors
+
+ public DiscountController(
+ IDiscountViewModelService discountViewModelService,
+ IDiscountService discountService,
+ ITranslationService translationService,
+ IContextAccessor contextAccessor,
+ IDateTimeService dateTimeService,
+ IMediator mediator)
+ {
+ _discountViewModelService = discountViewModelService;
+ _discountService = discountService;
+ _translationService = translationService;
+ _contextAccessor = contextAccessor;
+ _dateTimeService = dateTimeService;
+ _mediator = mediator;
+ }
+
+ #endregion
+
+ #region Fields
+
+ private readonly IDiscountViewModelService _discountViewModelService;
+ private readonly IDiscountService _discountService;
+ private readonly ITranslationService _translationService;
+ private readonly IContextAccessor _contextAccessor;
+ private readonly IDateTimeService _dateTimeService;
+ private readonly IMediator _mediator;
+
+ #endregion
+
+ #region Utilities
+
+ private string CurrentStoreId =>
+ _contextAccessor.WorkContext.CurrentCustomer.StaffStoreId;
+
+ #endregion
+
+ #region Discounts
+
+ public IActionResult Index()
+ {
+ return RedirectToAction("List");
+ }
+
+ public IActionResult List()
+ {
+ var model = _discountViewModelService.PrepareDiscountListModel();
+ return View(model);
+ }
+
+ [HttpPost]
+ [PermissionAuthorizeAction(PermissionActionName.List)]
+ public async Task List(DiscountListModel model, DataSourceRequest command)
+ {
+ var (discountModel, totalCount) =
+ await _discountViewModelService.PrepareDiscountModel(model, command.Page, command.PageSize);
+ var gridModel = new DataSourceResult {
+ Data = discountModel.ToList(),
+ Total = totalCount
+ };
+ return Json(gridModel);
+ }
+
+ [PermissionAuthorizeAction(PermissionActionName.Create)]
+ public async Task Create()
+ {
+ var model = new DiscountModel();
+ await _discountViewModelService.PrepareDiscountModel(model, null);
+ model.LimitationTimes = 1;
+ return View(model);
+ }
+
+ [HttpPost]
+ [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")]
+ [PermissionAuthorizeAction(PermissionActionName.Create)]
+ public async Task Create(DiscountModel model, bool continueEditing)
+ {
+ if (ModelState.IsValid)
+ {
+ model.Stores = [CurrentStoreId];
+ var discount = await _discountViewModelService.InsertDiscountModel(model);
+ Success(_translationService.GetResource("admin.marketing.discounts.Added"));
+ return continueEditing
+ ? RedirectToAction("Edit", new { id = discount.Id })
+ : RedirectToAction("List");
+ }
+
+ await _discountViewModelService.PrepareDiscountModel(model, null);
+ return View(model);
+ }
+
+ [PermissionAuthorizeAction(PermissionActionName.Preview)]
+ public async Task Edit(string id)
+ {
+ var discount = await _discountService.GetDiscountById(id);
+ if (discount == null)
+ return RedirectToAction("List");
+
+ if (!discount.AccessToEntityByStore(CurrentStoreId))
+ return RedirectToAction("List");
+
+ var model = discount.ToModel(_dateTimeService);
+ await _discountViewModelService.PrepareDiscountModel(model, discount);
+ return View(model);
+ }
+
+ [HttpPost]
+ [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")]
+ [PermissionAuthorizeAction(PermissionActionName.Edit)]
+ public async Task Edit(DiscountModel model, bool continueEditing)
+ {
+ var discount = await _discountService.GetDiscountById(model.Id);
+ if (discount == null)
+ return RedirectToAction("List");
+
+ if (!discount.AccessToEntityByStore(CurrentStoreId))
+ return RedirectToAction("Edit", new { id = discount.Id });
+
+ if (ModelState.IsValid)
+ {
+ model.Stores = [CurrentStoreId];
+ discount = await _discountViewModelService.UpdateDiscountModel(discount, model);
+ Success(_translationService.GetResource("admin.marketing.discounts.Updated"));
+ if (continueEditing)
+ {
+ await SaveSelectedTabIndex();
+ return RedirectToAction("Edit", new { id = discount.Id });
+ }
+
+ return RedirectToAction("List");
+ }
+
+ await _discountViewModelService.PrepareDiscountModel(model, discount);
+ return View(model);
+ }
+
+ [HttpPost]
+ [PermissionAuthorizeAction(PermissionActionName.Delete)]
+ public async Task Delete(string id)
+ {
+ var discount = await _discountService.GetDiscountById(id);
+ if (discount == null)
+ return RedirectToAction("List");
+
+ if (!discount.AccessToEntityByStore(CurrentStoreId))
+ return RedirectToAction("Edit", new { id });
+
+ var usageHistory = await _mediator.Send(new GetDiscountUsageHistoryQuery { DiscountId = discount.Id });
+ if (usageHistory.Count > 0)
+ {
+ Error(_translationService.GetResource("admin.marketing.discounts.Deleted.UsageHistory"));
+ return RedirectToAction("Edit", new { id = discount.Id });
+ }
+
+ if (ModelState.IsValid)
+ {
+ await _discountViewModelService.DeleteDiscount(discount);
+ Success(_translationService.GetResource("admin.marketing.discounts.Deleted"));
+ return RedirectToAction("List");
+ }
+
+ Error(ModelState);
+ return RedirectToAction("Edit", new { id = discount.Id });
+ }
+
+ #endregion
+
+ #region Discount coupon codes
+
+ [HttpPost]
+ [PermissionAuthorizeAction(PermissionActionName.Preview)]
+ public async Task CouponCodeList(DataSourceRequest command, string discountId)
+ {
+ var discount = await _discountService.GetDiscountById(discountId);
+ if (discount == null)
+ throw new Exception("No discount found with the specified id");
+
+ if (!discount.AccessToEntityByStore(CurrentStoreId))
+ return new JsonResult(new DataSourceResult { Errors = "Access denied" });
+
+ var couponcodes = await _discountService.GetAllCouponCodesByDiscountId(discount.Id,
+ command.Page - 1, command.PageSize);
+ var gridModel = new DataSourceResult {
+ Data = couponcodes.Select(x => new {
+ x.Id,
+ x.CouponCode,
+ x.Used
+ }),
+ Total = couponcodes.TotalCount
+ };
+ return Json(gridModel);
+ }
+
+ [PermissionAuthorizeAction(PermissionActionName.Edit)]
+ public async Task CouponCodeDelete(string discountId, string Id)
+ {
+ var discount = await _discountService.GetDiscountById(discountId);
+ if (discount == null)
+ throw new Exception("No discount found with the specified id");
+
+ if (!discount.AccessToEntityByStore(CurrentStoreId))
+ return new JsonResult(new DataSourceResult { Errors = "Access denied" });
+
+ var coupon = await _discountService.GetDiscountCodeById(Id);
+ if (coupon == null)
+ throw new Exception("No coupon code found with the specified id");
+ if (ModelState.IsValid)
+ {
+ if (!coupon.Used)
+ await _discountService.DeleteDiscountCoupon(coupon);
+ else
+ return new JsonResult(new DataSourceResult
+ { Errors = "You can't delete coupon code, it was used" });
+
+ return new JsonResult("");
+ }
+
+ return ErrorForKendoGridJson(ModelState);
+ }
+
+ [PermissionAuthorizeAction(PermissionActionName.Edit)]
+ public async Task CouponCodeInsert(string discountId, string couponCode)
+ {
+ if (string.IsNullOrEmpty(couponCode))
+ throw new Exception("Coupon code can't be empty");
+
+ var discount = await _discountService.GetDiscountById(discountId);
+ if (discount == null)
+ throw new Exception("No discount found with the specified id");
+
+ if (!discount.AccessToEntityByStore(CurrentStoreId))
+ return new JsonResult(new DataSourceResult { Errors = "Access denied" });
+
+ couponCode = couponCode.ToUpper();
+
+ if (await _discountService.GetDiscountByCouponCode(couponCode) != null)
+ return new JsonResult(new DataSourceResult { Errors = "Coupon code exists" });
+ if (ModelState.IsValid)
+ {
+ await _discountViewModelService.InsertCouponCode(discountId, couponCode);
+ return new JsonResult("");
+ }
+
+ return ErrorForKendoGridJson(ModelState);
+ }
+
+ #endregion
+
+ #region Discount requirements
+
+ [AcceptVerbs("GET")]
+ [PermissionAuthorizeAction(PermissionActionName.Preview)]
+ public async Task GetDiscountRequirementConfigurationUrl(string rulesystemName,
+ string discountId, string discountRequirementId,
+ [FromServices] IDiscountProviderLoader discountProviderLoader)
+ {
+ ArgumentNullException.ThrowIfNullOrEmpty(rulesystemName);
+
+ var discountPlugin = discountProviderLoader.LoadDiscountProviderByRuleSystemName(rulesystemName);
+
+ if (discountPlugin == null)
+ throw new ArgumentException("Discount requirement rule could not be loaded");
+
+ var discount = await _discountService.GetDiscountById(discountId);
+ if (discount == null)
+ throw new ArgumentException("Discount could not be loaded");
+
+ if (!discount.AccessToEntityByStore(CurrentStoreId))
+ throw new ArgumentException("Access denied");
+
+ var singleRequirement = discountPlugin.GetRequirementRules().FirstOrDefault(x =>
+ x.SystemName.Equals(rulesystemName, StringComparison.OrdinalIgnoreCase));
+ var url = _discountViewModelService.GetRequirementUrlInternal(singleRequirement, discount,
+ discountRequirementId);
+ return Json(new { url });
+ }
+
+ [PermissionAuthorizeAction(PermissionActionName.Preview)]
+ public async Task GetDiscountRequirementMetaInfo(string discountRequirementId, string discountId,
+ [FromServices] IDiscountProviderLoader discountProviderLoader)
+ {
+ var discount = await _discountService.GetDiscountById(discountId);
+ if (discount == null)
+ throw new ArgumentException("Discount could not be loaded");
+
+ if (!discount.AccessToEntityByStore(CurrentStoreId))
+ throw new ArgumentException("Access denied");
+
+ var discountRequirement = discount.DiscountRules.FirstOrDefault(dr => dr.Id == discountRequirementId);
+ if (discountRequirement == null)
+ throw new ArgumentException("Discount requirement could not be loaded");
+
+ var discountPlugin =
+ discountProviderLoader.LoadDiscountProviderByRuleSystemName(discountRequirement
+ .DiscountRequirementRuleSystemName);
+ if (discountPlugin == null)
+ throw new ArgumentException("Discount requirement rule could not be loaded");
+
+ var discountRequirementRule = discountPlugin.GetRequirementRules()
+ .First(x => x.SystemName == discountRequirement.DiscountRequirementRuleSystemName);
+ var url = _discountViewModelService.GetRequirementUrlInternal(discountRequirementRule, discount,
+ discountRequirementId);
+ var ruleName = discountRequirementRule.FriendlyName;
+
+ return Json(new { url, ruleName });
+ }
+
+ [HttpPost]
+ [PermissionAuthorizeAction(PermissionActionName.Edit)]
+ public async Task DeleteDiscountRequirement(string discountRequirementId, string discountId)
+ {
+ var discount = await _discountService.GetDiscountById(discountId);
+ if (discount == null)
+ throw new ArgumentException("Discount could not be loaded");
+
+ if (!discount.AccessToEntityByStore(CurrentStoreId))
+ return Json(new { Result = false, Error = "Access denied" });
+
+ var discountRequirement = discount.DiscountRules.FirstOrDefault(dr => dr.Id == discountRequirementId);
+ if (discountRequirement == null)
+ throw new ArgumentException("Discount requirement could not be loaded");
+
+ if (ModelState.IsValid)
+ {
+ await _discountViewModelService.DeleteDiscountRequirement(discountRequirement, discount);
+ return Json(new { Result = true });
+ }
+
+ return ErrorForKendoGridJson(ModelState);
+ }
+
+ #endregion
+
+ #region Applied to products
+
+ [PermissionAuthorizeAction(PermissionActionName.Preview)]
+ [HttpPost]
+ public async Task ProductList(DataSourceRequest command, string discountId,
+ [FromServices] IProductService productService)
+ {
+ var discount = await _discountService.GetDiscountById(discountId);
+ if (discount == null)
+ throw new Exception("No discount found with the specified id");
+
+ if (!discount.AccessToEntityByStore(CurrentStoreId))
+ return new JsonResult(new DataSourceResult { Errors = "Access denied" });
+
+ var products = await productService.GetProductsByDiscount(discount.Id, command.Page - 1,
+ command.PageSize);
+ var gridModel = new DataSourceResult {
+ Data = products.Select(x => new DiscountModel.AppliedToProductModel {
+ ProductId = x.Id,
+ ProductName = x.Name
+ }),
+ Total = products.TotalCount
+ };
+
+ return Json(gridModel);
+ }
+
+ [PermissionAuthorizeAction(PermissionActionName.Edit)]
+ public async Task ProductDelete(string discountId, string productId,
+ [FromServices] IProductService productService)
+ {
+ var discount = await _discountService.GetDiscountById(discountId);
+ if (discount == null)
+ throw new Exception("No discount found with the specified id");
+
+ if (!discount.AccessToEntityByStore(CurrentStoreId))
+ return new JsonResult(new DataSourceResult { Errors = "Access denied" });
+
+ var product = await productService.GetProductById(productId);
+ if (product == null)
+ throw new Exception("No product found with the specified id");
+
+ if (ModelState.IsValid)
+ {
+ await _discountViewModelService.DeleteProduct(discount, product);
+ return new JsonResult("");
+ }
+
+ return ErrorForKendoGridJson(ModelState);
+ }
+
+ [PermissionAuthorizeAction(PermissionActionName.Edit)]
+ public async Task ProductAddPopup(string discountId)
+ {
+ var model = await _discountViewModelService.PrepareProductToDiscountModel();
+ return View(model);
+ }
+
+ [PermissionAuthorizeAction(PermissionActionName.Edit)]
+ [HttpPost]
+ public async Task ProductAddPopupList(DataSourceRequest command,
+ DiscountModel.AddProductToDiscountModel model)
+ {
+ model.SearchStoreId = CurrentStoreId;
+ var products = await _discountViewModelService.PrepareProductModel(model, command.Page, command.PageSize);
+
+ var gridModel = new DataSourceResult {
+ Data = products.products.ToList(),
+ Total = products.totalCount
+ };
+
+ return Json(gridModel);
+ }
+
+ [PermissionAuthorizeAction(PermissionActionName.Edit)]
+ [HttpPost]
+ public async Task ProductAddPopup(DiscountModel.AddProductToDiscountModel model)
+ {
+ var discount = await _discountService.GetDiscountById(model.DiscountId);
+ if (discount == null)
+ throw new Exception("No discount found with the specified id");
+
+ if (!discount.AccessToEntityByStore(CurrentStoreId))
+ return Content("Access denied");
+
+ if (model.SelectedProductIds != null) await _discountViewModelService.InsertProductToDiscountModel(model);
+
+ return Content("");
+ }
+
+ #endregion
+
+ #region Applied to categories
+
+ [PermissionAuthorizeAction(PermissionActionName.Preview)]
+ [HttpPost]
+ public async Task CategoryList(DataSourceRequest command, string discountId,
+ [FromServices] ICategoryService categoryService)
+ {
+ var discount = await _discountService.GetDiscountById(discountId);
+ if (discount == null)
+ throw new Exception("No discount found with the specified id");
+
+ if (!discount.AccessToEntityByStore(CurrentStoreId))
+ return new JsonResult(new DataSourceResult { Errors = "Access denied" });
+
+ var categories = await categoryService.GetAllCategoriesByDiscount(discount.Id);
+ var items = new List();
+ foreach (var item in categories)
+ items.Add(new DiscountModel.AppliedToCategoryModel {
+ CategoryId = item.Id,
+ CategoryName = await categoryService.GetFormattedBreadCrumb(item)
+ });
+
+ var gridModel = new DataSourceResult {
+ Data = items,
+ Total = categories.Count
+ };
+
+ return Json(gridModel);
+ }
+
+ [PermissionAuthorizeAction(PermissionActionName.Edit)]
+ public async Task CategoryDelete(string discountId, string categoryId,
+ [FromServices] ICategoryService categoryService)
+ {
+ var discount = await _discountService.GetDiscountById(discountId);
+ if (discount == null)
+ throw new Exception("No discount found with the specified id");
+
+ if (!discount.AccessToEntityByStore(CurrentStoreId))
+ return new JsonResult(new DataSourceResult { Errors = "Access denied" });
+
+ var category = await categoryService.GetCategoryById(categoryId);
+ if (category == null)
+ throw new Exception("No category found with the specified id");
+
+ if (ModelState.IsValid)
+ {
+ await _discountViewModelService.DeleteCategory(discount, category);
+ return new JsonResult("");
+ }
+
+ return ErrorForKendoGridJson(ModelState);
+ }
+
+ [PermissionAuthorizeAction(PermissionActionName.Edit)]
+ public IActionResult CategoryAddPopup(string discountId)
+ {
+ var model = new DiscountModel.AddCategoryToDiscountModel();
+ return View(model);
+ }
+
+ [PermissionAuthorizeAction(PermissionActionName.Edit)]
+ [HttpPost]
+ public async Task CategoryAddPopupList(DataSourceRequest command,
+ DiscountModel.AddCategoryToDiscountModel model, [FromServices] ICategoryService categoryService)
+ {
+ var categories = await categoryService.GetAllCategories(categoryName: model.SearchCategoryName,
+ pageIndex: command.Page - 1, pageSize: command.PageSize, showHidden: true);
+ var items = new List();
+ foreach (var item in categories)
+ {
+ var categoryModel = item.ToModel();
+ categoryModel.Breadcrumb = await categoryService.GetFormattedBreadCrumb(item);
+ items.Add(categoryModel);
+ }
+
+ var gridModel = new DataSourceResult {
+ Data = items,
+ Total = categories.TotalCount
+ };
+
+ return Json(gridModel);
+ }
+
+ [PermissionAuthorizeAction(PermissionActionName.Edit)]
+ [HttpPost]
+ public async Task CategoryAddPopup(DiscountModel.AddCategoryToDiscountModel model)
+ {
+ var discount = await _discountService.GetDiscountById(model.DiscountId);
+ if (discount == null)
+ throw new Exception("No discount found with the specified id");
+
+ if (!discount.AccessToEntityByStore(CurrentStoreId))
+ return Content("Access denied");
+
+ if (model.SelectedCategoryIds != null) await _discountViewModelService.InsertCategoryToDiscountModel(model);
+
+ return Content("");
+ }
+
+ #endregion
+
+ #region Applied to brands
+
+ [PermissionAuthorizeAction(PermissionActionName.Preview)]
+ [HttpPost]
+ public async Task BrandList(DataSourceRequest command, string discountId,
+ [FromServices] IBrandService brandService)
+ {
+ var discount = await _discountService.GetDiscountById(discountId);
+ if (discount == null)
+ throw new Exception("No discount found with the specified id");
+
+ if (!discount.AccessToEntityByStore(CurrentStoreId))
+ return new JsonResult(new DataSourceResult { Errors = "Access denied" });
+
+ var brands = await brandService.GetAllBrandsByDiscount(discount.Id);
+ var gridModel = new DataSourceResult {
+ Data = brands.Select(x => new DiscountModel.AppliedToBrandModel {
+ BrandId = x.Id,
+ BrandName = x.Name
+ }),
+ Total = brands.Count
+ };
+
+ return Json(gridModel);
+ }
+
+ [PermissionAuthorizeAction(PermissionActionName.Edit)]
+ public async Task BrandDelete(string discountId, string brandId,
+ [FromServices] IBrandService brandService)
+ {
+ var discount = await _discountService.GetDiscountById(discountId);
+ if (discount == null)
+ throw new Exception("No discount found with the specified id");
+
+ if (!discount.AccessToEntityByStore(CurrentStoreId))
+ return new JsonResult(new DataSourceResult { Errors = "Access denied" });
+
+ var brand = await brandService.GetBrandById(brandId);
+ if (brand == null)
+ throw new Exception("No brand found with the specified id");
+
+ if (ModelState.IsValid)
+ {
+ await _discountViewModelService.DeleteBrand(discount, brand);
+ return new JsonResult("");
+ }
+
+ return ErrorForKendoGridJson(ModelState);
+ }
+
+ [PermissionAuthorizeAction(PermissionActionName.Edit)]
+ public IActionResult BrandAddPopup(string discountId)
+ {
+ var model = new DiscountModel.AddBrandToDiscountModel();
+ return View(model);
+ }
+
+ [PermissionAuthorizeAction(PermissionActionName.Edit)]
+ [HttpPost]
+ public async Task BrandAddPopupList(DataSourceRequest command,
+ DiscountModel.AddBrandToDiscountModel model, [FromServices] IBrandService brandService)
+ {
+ var brands = await brandService.GetAllBrands(model.SearchBrandName,
+ CurrentStoreId, command.Page - 1, command.PageSize, true);
+
+ var gridModel = new DataSourceResult {
+ Data = brands.Select(x => x.ToModel()),
+ Total = brands.TotalCount
+ };
+
+ return Json(gridModel);
+ }
+
+ [PermissionAuthorizeAction(PermissionActionName.Edit)]
+ [HttpPost]
+ public async Task BrandAddPopup(DiscountModel.AddBrandToDiscountModel model)
+ {
+ var discount = await _discountService.GetDiscountById(model.DiscountId);
+ if (discount == null)
+ throw new Exception("No discount found with the specified id");
+
+ if (!discount.AccessToEntityByStore(CurrentStoreId))
+ return Content("Access denied");
+
+ if (model.SelectedBrandIds != null) await _discountViewModelService.InsertBrandToDiscountModel(model);
+
+ return Content("");
+ }
+
+ #endregion
+
+ #region Applied to collections
+
+ [PermissionAuthorizeAction(PermissionActionName.Preview)]
+ [HttpPost]
+ public async Task CollectionList(DataSourceRequest command, string discountId,
+ [FromServices] ICollectionService collectionService)
+ {
+ var discount = await _discountService.GetDiscountById(discountId);
+ if (discount == null)
+ throw new Exception("No discount found with the specified id");
+
+ if (!discount.AccessToEntityByStore(CurrentStoreId))
+ return new JsonResult(new DataSourceResult { Errors = "Access denied" });
+
+ var collections = await collectionService.GetAllCollectionsByDiscount(discount.Id);
+ var gridModel = new DataSourceResult {
+ Data = collections.Select(x => new DiscountModel.AppliedToCollectionModel {
+ CollectionId = x.Id,
+ CollectionName = x.Name
+ }),
+ Total = collections.Count
+ };
+
+ return Json(gridModel);
+ }
+
+ [PermissionAuthorizeAction(PermissionActionName.Edit)]
+ public async Task CollectionDelete(string discountId, string collectionId,
+ [FromServices] ICollectionService collectionService)
+ {
+ var discount = await _discountService.GetDiscountById(discountId);
+ if (discount == null)
+ throw new Exception("No discount found with the specified id");
+
+ if (!discount.AccessToEntityByStore(CurrentStoreId))
+ return new JsonResult(new DataSourceResult { Errors = "Access denied" });
+
+ var collection = await collectionService.GetCollectionById(collectionId);
+ if (collection == null)
+ throw new Exception("No collection found with the specified id");
+
+ if (ModelState.IsValid)
+ {
+ await _discountViewModelService.DeleteCollection(discount, collection);
+ return new JsonResult("");
+ }
+
+ return ErrorForKendoGridJson(ModelState);
+ }
+
+ [PermissionAuthorizeAction(PermissionActionName.Edit)]
+ public IActionResult CollectionAddPopup(string discountId)
+ {
+ var model = new DiscountModel.AddCollectionToDiscountModel();
+ return View(model);
+ }
+
+ [PermissionAuthorizeAction(PermissionActionName.Edit)]
+ [HttpPost]
+ public async Task CollectionAddPopupList(DataSourceRequest command,
+ DiscountModel.AddCollectionToDiscountModel model, [FromServices] ICollectionService collectionService)
+ {
+ var collections = await collectionService.GetAllCollections(model.SearchCollectionName, CurrentStoreId,
+ command.Page - 1, command.PageSize, true);
+ var gridModel = new DataSourceResult {
+ Data = collections.Select(x => x.ToModel()),
+ Total = collections.TotalCount
+ };
+
+ return Json(gridModel);
+ }
+
+ [PermissionAuthorizeAction(PermissionActionName.Edit)]
+ [HttpPost]
+ public async Task CollectionAddPopup(DiscountModel.AddCollectionToDiscountModel model)
+ {
+ var discount = await _discountService.GetDiscountById(model.DiscountId);
+ if (discount == null)
+ throw new Exception("No discount found with the specified id");
+
+ if (!discount.AccessToEntityByStore(CurrentStoreId))
+ return Content("Access denied");
+
+ if (model.SelectedCollectionIds != null) await _discountViewModelService.InsertCollectionToDiscountModel(model);
+
+ return Content("");
+ }
+
+ #endregion
+
+ #region Discount usage history
+
+ [PermissionAuthorizeAction(PermissionActionName.Preview)]
+ [HttpPost]
+ public async Task UsageHistoryList(string discountId, DataSourceRequest command)
+ {
+ var discount = await _discountService.GetDiscountById(discountId);
+ if (discount == null)
+ throw new ArgumentException("No discount found with the specified id");
+
+ if (!discount.AccessToEntityByStore(CurrentStoreId))
+ return new JsonResult(new DataSourceResult { Errors = "Access denied" });
+
+ var (usageHistoryModels, totalCount) =
+ await _discountViewModelService.PrepareDiscountUsageHistoryModel(discount, command.Page,
+ command.PageSize);
+ var gridModel = new DataSourceResult {
+ Data = usageHistoryModels.ToList(),
+ Total = totalCount
+ };
+ return Json(gridModel);
+ }
+
+ [PermissionAuthorizeAction(PermissionActionName.Edit)]
+ [HttpPost]
+ public async Task UsageHistoryDelete(string discountId, string id)
+ {
+ var discount = await _discountService.GetDiscountById(discountId);
+ if (discount == null)
+ throw new ArgumentException("No discount found with the specified id");
+
+ if (!discount.AccessToEntityByStore(CurrentStoreId))
+ return new JsonResult(new DataSourceResult { Errors = "Access denied" });
+
+ var duh = await _discountService.GetDiscountUsageHistoryById(id);
+ if (duh != null)
+ {
+ if (ModelState.IsValid)
+ await _discountService.DeleteDiscountUsageHistory(duh);
+ else
+ return ErrorForKendoGridJson(ModelState);
+ }
+
+ return new JsonResult("");
+ }
+
+ #endregion
+}
From 8cc6221f86ed843da1d05758e630da70e375a779 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 31 May 2026 17:46:42 +0000
Subject: [PATCH 02/16] Fix btnRefreshBrands input type to submit for
consistency
Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com>
---
.../Discount/Partials/CreateOrUpdate.TabAppliedToBrands.cshtml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.TabAppliedToBrands.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.TabAppliedToBrands.cshtml
index b2df80bff..e40b3c89f 100644
--- a/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.TabAppliedToBrands.cshtml
+++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Discount/Partials/CreateOrUpdate.TabAppliedToBrands.cshtml
@@ -8,7 +8,7 @@
From 21328c7f0d23ddae77ddf2acee2bf610d270f998 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 31 May 2026 19:26:03 +0000
Subject: [PATCH 03/16] Fix Store-area discount requirements by adding
Store-area plugin controllers and views
Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com>
---
.../Controllers/CustomerGroupsController.cs | 101 ++++++++
.../Controllers/HadSpentAmountController.cs | 91 +++++++
.../Controllers/HasAllProductsController.cs | 227 ++++++++++++++++++
.../Controllers/HasOneProductController.cs | 207 ++++++++++++++++
.../ShoppingCartAmountController.cs | 89 +++++++
.../Views/CustomerGroups/Configure.cshtml | 54 +++++
.../Views/HadSpentAmount/Configure.cshtml | 53 ++++
.../Views/HasAllProducts/Configure.cshtml | 126 ++++++++++
.../HasAllProducts/ProductAddPopup.cshtml | 193 +++++++++++++++
.../Views/HasOneProduct/Configure.cshtml | 125 ++++++++++
.../HasOneProduct/ProductAddPopup.cshtml | 192 +++++++++++++++
.../Views/ShoppingCartAmount/Configure.cshtml | 45 ++++
.../Areas/Store/Views/_ViewImports.cshtml | 11 +
.../Controllers/BaseStorePluginController.cs | 12 +
.../Controllers/DiscountController.cs | 16 +-
15 files changed, 1538 insertions(+), 4 deletions(-)
create mode 100644 src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/CustomerGroupsController.cs
create mode 100644 src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/HadSpentAmountController.cs
create mode 100644 src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/HasAllProductsController.cs
create mode 100644 src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/HasOneProductController.cs
create mode 100644 src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/ShoppingCartAmountController.cs
create mode 100644 src/Plugins/DiscountRules.Standard/Areas/Store/Views/CustomerGroups/Configure.cshtml
create mode 100644 src/Plugins/DiscountRules.Standard/Areas/Store/Views/HadSpentAmount/Configure.cshtml
create mode 100644 src/Plugins/DiscountRules.Standard/Areas/Store/Views/HasAllProducts/Configure.cshtml
create mode 100644 src/Plugins/DiscountRules.Standard/Areas/Store/Views/HasAllProducts/ProductAddPopup.cshtml
create mode 100644 src/Plugins/DiscountRules.Standard/Areas/Store/Views/HasOneProduct/Configure.cshtml
create mode 100644 src/Plugins/DiscountRules.Standard/Areas/Store/Views/HasOneProduct/ProductAddPopup.cshtml
create mode 100644 src/Plugins/DiscountRules.Standard/Areas/Store/Views/ShoppingCartAmount/Configure.cshtml
create mode 100644 src/Plugins/DiscountRules.Standard/Areas/Store/Views/_ViewImports.cshtml
create mode 100644 src/Web/Grand.Web.Common/Controllers/BaseStorePluginController.cs
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/CustomerGroupsController.cs b/src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/CustomerGroupsController.cs
new file mode 100644
index 000000000..ebeaf8c3a
--- /dev/null
+++ b/src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/CustomerGroupsController.cs
@@ -0,0 +1,101 @@
+using DiscountRules.Standard.Models;
+using Grand.Business.Core.Interfaces.Catalog.Discounts;
+using Grand.Business.Core.Interfaces.Common.Directory;
+using Grand.Business.Core.Interfaces.Common.Security;
+using Grand.Domain.Permissions;
+using Grand.Domain.Discounts;
+using Grand.Web.Common.Controllers;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Rendering;
+
+namespace DiscountRules.Standard.Areas.Store.Controllers;
+
+public class CustomerGroupsController : BaseStorePluginController
+{
+ private readonly IDiscountService _discountService;
+ private readonly IGroupService _groupService;
+ private readonly IPermissionService _permissionService;
+
+ public CustomerGroupsController(
+ IDiscountService discountService,
+ IGroupService groupService,
+ IPermissionService permissionService)
+ {
+ _discountService = discountService;
+ _groupService = groupService;
+ _permissionService = permissionService;
+ }
+
+ public async Task Configure(string discountId, string discountRequirementId)
+ {
+ if (!await _permissionService.Authorize(StandardPermission.ManageDiscounts))
+ return Content("Access denied");
+
+ var discount = await _discountService.GetDiscountById(discountId);
+ if (discount == null)
+ throw new ArgumentException("Discount could not be loaded");
+
+ DiscountRule discountRequirement = null;
+ if (!string.IsNullOrEmpty(discountRequirementId))
+ {
+ discountRequirement = discount.DiscountRules.FirstOrDefault(dr => dr.Id == discountRequirementId);
+ if (discountRequirement == null)
+ return Content("Failed to load requirement.");
+ }
+
+ var model = new RequirementCustomerGroupsModel {
+ RequirementId = !string.IsNullOrEmpty(discountRequirementId) ? discountRequirementId : "",
+ DiscountId = discountId,
+ CustomerGroupId = discountRequirement?.Metadata
+ };
+
+ //customer groups
+ model.AvailableCustomerGroups.Add(new SelectListItem { Text = "Select customer group", Value = "" });
+ foreach (var cr in await _groupService.GetAllCustomerGroups(showHidden: true))
+ model.AvailableCustomerGroups.Add(new SelectListItem {
+ Text = cr.Name, Value = cr.Id,
+ Selected = discountRequirement != null && cr.Id == discountRequirement.Metadata
+ });
+
+ //add a prefix
+ ViewData.TemplateInfo.HtmlFieldPrefix =
+ $"DiscountRulesCustomerGroups{(!string.IsNullOrEmpty(discountRequirementId) ? discountRequirementId : "")}";
+
+ return View(model);
+ }
+
+ [HttpPost]
+ [AutoValidateAntiforgeryToken]
+ public async Task Configure(string discountId, string discountRequirementId, string customerGroupId)
+ {
+ if (!await _permissionService.Authorize(StandardPermission.ManageDiscounts))
+ return Content("Access denied");
+
+ var discount = await _discountService.GetDiscountById(discountId);
+ if (discount == null)
+ throw new ArgumentException("Discount could not be loaded");
+
+ DiscountRule discountRequirement = null;
+ if (!string.IsNullOrEmpty(discountRequirementId))
+ discountRequirement = discount.DiscountRules.FirstOrDefault(dr => dr.Id == discountRequirementId);
+
+ if (discountRequirement != null)
+ {
+ //update existing rule
+ discountRequirement.Metadata = customerGroupId;
+ await _discountService.UpdateDiscount(discount);
+ }
+ else
+ {
+ //save new rule
+ discountRequirement = new DiscountRule {
+ DiscountRequirementRuleSystemName = "DiscountRules.Standard.MustBeAssignedToCustomerGroup",
+ Metadata = customerGroupId
+ };
+ discount.DiscountRules.Add(discountRequirement);
+ await _discountService.UpdateDiscount(discount);
+ }
+
+ return Json(new { Result = true, NewRequirementId = discountRequirement.Id });
+ }
+}
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/HadSpentAmountController.cs b/src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/HadSpentAmountController.cs
new file mode 100644
index 000000000..c03bdd7d6
--- /dev/null
+++ b/src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/HadSpentAmountController.cs
@@ -0,0 +1,91 @@
+using DiscountRules.Standard.Models;
+using Grand.Business.Core.Interfaces.Catalog.Discounts;
+using Grand.Business.Core.Interfaces.Common.Security;
+using Grand.Domain.Permissions;
+using Grand.Domain.Discounts;
+using Grand.Web.Common.Controllers;
+using Microsoft.AspNetCore.Mvc;
+using System.Globalization;
+
+namespace DiscountRules.Standard.Areas.Store.Controllers;
+
+public class HadSpentAmountController : BaseStorePluginController
+{
+ private readonly IDiscountService _discountService;
+ private readonly IPermissionService _permissionService;
+
+ public HadSpentAmountController(
+ IDiscountService discountService,
+ IPermissionService permissionService)
+ {
+ _discountService = discountService;
+ _permissionService = permissionService;
+ }
+
+ public async Task Configure(string discountId, string discountRequirementId)
+ {
+ if (!await _permissionService.Authorize(StandardPermission.ManageDiscounts))
+ return Content("Access denied");
+
+ var discount = await _discountService.GetDiscountById(discountId);
+ if (discount == null)
+ throw new ArgumentException("Discount could not be loaded");
+
+ double spentAmountRequirement = 0;
+ if (!string.IsNullOrEmpty(discountRequirementId))
+ {
+ var discountRequirement = discount.DiscountRules.FirstOrDefault(dr => dr.Id == discountRequirementId);
+ if (discountRequirement == null)
+ return Content("Failed to load requirement.");
+
+ spentAmountRequirement = Convert.ToDouble(discountRequirement.Metadata);
+ }
+
+ var model = new RequirementSpentAmountModel {
+ RequirementId = !string.IsNullOrEmpty(discountRequirementId) ? discountRequirementId : "",
+ DiscountId = discountId,
+ SpentAmount = spentAmountRequirement
+ };
+
+ //add a prefix
+ ViewData.TemplateInfo.HtmlFieldPrefix =
+ $"DiscountRulesHadSpentAmount{discount.Id}-{(!string.IsNullOrEmpty(discountRequirementId) ? discountRequirementId : "")}";
+
+ return View(model);
+ }
+
+ [HttpPost]
+ [AutoValidateAntiforgeryToken]
+ public async Task Configure(string discountId, string discountRequirementId, double spentAmount)
+ {
+ if (!await _permissionService.Authorize(StandardPermission.ManageDiscounts))
+ return Content("Access denied");
+
+ var discount = await _discountService.GetDiscountById(discountId);
+ if (discount == null)
+ throw new ArgumentException("Discount could not be loaded");
+
+ DiscountRule discountRequirement = null;
+ if (!string.IsNullOrEmpty(discountRequirementId))
+ discountRequirement = discount.DiscountRules.FirstOrDefault(dr => dr.Id == discountRequirementId);
+
+ if (discountRequirement != null)
+ {
+ //update existing rule
+ discountRequirement.Metadata = spentAmount.ToString(CultureInfo.InvariantCulture);
+ await _discountService.UpdateDiscount(discount);
+ }
+ else
+ {
+ //save new rule
+ discountRequirement = new DiscountRule {
+ DiscountRequirementRuleSystemName = "DiscountRules.Standard.HadSpentAmount",
+ Metadata = spentAmount.ToString(CultureInfo.InvariantCulture)
+ };
+ discount.DiscountRules.Add(discountRequirement);
+ await _discountService.UpdateDiscount(discount);
+ }
+
+ return new JsonResult(new { Result = true, NewRequirementId = discountRequirement.Id });
+ }
+}
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/HasAllProductsController.cs b/src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/HasAllProductsController.cs
new file mode 100644
index 000000000..98e20d7dd
--- /dev/null
+++ b/src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/HasAllProductsController.cs
@@ -0,0 +1,227 @@
+using DiscountRules.Standard.Models;
+using Grand.Business.Core.Interfaces.Catalog.Discounts;
+using Grand.Business.Core.Interfaces.Catalog.Products;
+using Grand.Business.Core.Interfaces.Common.Localization;
+using Grand.Business.Core.Interfaces.Common.Security;
+using Grand.Business.Core.Interfaces.Common.Stores;
+using Grand.Business.Core.Interfaces.Customers;
+using Grand.Domain.Catalog;
+using Grand.Domain.Discounts;
+using Grand.Domain.Permissions;
+using Grand.Infrastructure;
+using Grand.Web.Common.Controllers;
+using Grand.Web.Common.DataSource;
+using Grand.Web.Common.Localization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Rendering;
+
+namespace DiscountRules.Standard.Areas.Store.Controllers;
+
+public class HasAllProductsController : BaseStorePluginController
+{
+ private readonly IDiscountService _discountService;
+ private readonly IPermissionService _permissionService;
+ private readonly IProductService _productService;
+ private readonly IStoreService _storeService;
+ private readonly ITranslationService _translationService;
+ private readonly IVendorService _vendorService;
+ private readonly IContextAccessor _contextAccessor;
+ private readonly IEnumTranslationService _enumTranslationService;
+
+ private string CurrentStoreId => _contextAccessor.WorkContext.CurrentCustomer.StaffStoreId;
+
+ public HasAllProductsController(IDiscountService discountService,
+ IPermissionService permissionService,
+ IContextAccessor contextAccessor,
+ ITranslationService translationService,
+ IStoreService storeService,
+ IVendorService vendorService,
+ IProductService productService,
+ IEnumTranslationService enumTranslationService)
+ {
+ _discountService = discountService;
+ _permissionService = permissionService;
+ _contextAccessor = contextAccessor;
+ _translationService = translationService;
+ _storeService = storeService;
+ _vendorService = vendorService;
+ _productService = productService;
+ _enumTranslationService = enumTranslationService;
+ }
+
+ public async Task Configure(string discountId, string discountRequirementId)
+ {
+ if (!await AuthorizeManageDiscounts())
+ return Content("Access denied");
+
+ var discount = await GetDiscountById(discountId);
+ if (discount == null)
+ throw new ArgumentException("Discount could not be loaded");
+
+ var restrictedProductIds = string.Empty;
+
+ if (!string.IsNullOrEmpty(discountRequirementId))
+ {
+ var discountRequirement = discount.DiscountRules.FirstOrDefault(dr => dr.Id == discountRequirementId);
+ if (discountRequirement == null)
+ return Content("Failed to load requirement.");
+
+ restrictedProductIds = discountRequirement.Metadata;
+ }
+
+ var model = new RequirementAllProductsModel {
+ RequirementId = !string.IsNullOrEmpty(discountRequirementId) ? discountRequirementId : "",
+ DiscountId = discountId,
+ Products = restrictedProductIds
+ };
+
+ //add a prefix
+ ViewData.TemplateInfo.HtmlFieldPrefix =
+ $"DiscountRulesHasAllProducts{discount.Id}-{(!string.IsNullOrEmpty(discountRequirementId) ? discountRequirementId : "")}";
+
+ return View(model);
+ }
+
+ [HttpPost]
+ [AutoValidateAntiforgeryToken]
+ public async Task Configure(string discountId, string discountRequirementId, string productIds)
+ {
+ if (!await AuthorizeManageDiscounts())
+ return Content("Access denied");
+
+ var discount = await GetDiscountById(discountId);
+ if (discount == null)
+ throw new ArgumentException("Discount could not be loaded");
+
+ DiscountRule discountRequirement = null;
+
+ if (!string.IsNullOrEmpty(discountRequirementId))
+ discountRequirement = discount.DiscountRules.FirstOrDefault(dr => dr.Id == discountRequirementId);
+
+ if (discountRequirement != null)
+ {
+ //update existing rule
+ discountRequirement.Metadata = productIds;
+ await _discountService.UpdateDiscount(discount);
+ }
+ else
+ {
+ //save new rule
+ discountRequirement = new DiscountRule {
+ DiscountRequirementRuleSystemName = "DiscountRules.HasAllProducts",
+ Metadata = productIds
+ };
+ discount.DiscountRules.Add(discountRequirement);
+ await _discountService.UpdateDiscount(discount);
+ }
+
+ return new JsonResult(new { Result = true, NewRequirementId = discountRequirement.Id });
+ }
+
+ public async Task ProductAddPopup(string btnId, string productIdsInput)
+ {
+ if (!await AuthorizeManageProducts())
+ return Content("Access denied");
+
+ var model = new RequirementAllProductsModel.AddProductModel();
+
+ //stores - scoped to current store manager's store
+ model.AvailableStores.Add(new SelectListItem { Text = _translationService.GetResource("Admin.Common.All"), Value = "" });
+ foreach (var s in await _storeService.GetAllStores())
+ model.AvailableStores.Add(new SelectListItem { Text = s.Shortcut, Value = s.Id });
+
+ //product types
+ model.AvailableProductTypes = _enumTranslationService.ToSelectList(ProductType.SimpleProduct, false).ToList();
+ model.AvailableProductTypes.Insert(0,
+ new SelectListItem { Text = _translationService.GetResource("Admin.Common.All"), Value = "" });
+
+ ViewBag.productIdsInput = productIdsInput;
+ ViewBag.btnId = btnId;
+
+ return View(model);
+ }
+
+ [HttpPost]
+ [AutoValidateAntiforgeryToken]
+ public async Task ProductAddPopupList(DataSourceRequest command,
+ RequirementAllProductsModel.AddProductModel model)
+ {
+ if (!await AuthorizeManageProducts())
+ return Content("Access denied");
+
+ var searchCategoryIds = new List();
+ if (!string.IsNullOrEmpty(model.SearchCategoryId))
+ searchCategoryIds.Add(model.SearchCategoryId);
+
+ var products = (await _productService.SearchProducts(
+ categoryIds: searchCategoryIds,
+ collectionId: model.SearchCollectionId,
+ storeId: !string.IsNullOrEmpty(model.SearchStoreId) ? model.SearchStoreId : CurrentStoreId,
+ productType: model.SearchProductTypeId > 0 ? (ProductType?)model.SearchProductTypeId : null,
+ keywords: model.SearchProductName,
+ pageIndex: command.Page - 1,
+ pageSize: command.PageSize,
+ showHidden: true
+ )).products;
+ var gridModel = new DataSourceResult {
+ Data = products.Select(x => new RequirementAllProductsModel.ProductModel {
+ Id = x.Id,
+ Name = x.Name,
+ Published = x.Published
+ }),
+ Total = products.TotalCount
+ };
+
+ return new JsonResult(gridModel);
+ }
+
+ [HttpPost]
+ [AutoValidateAntiforgeryToken]
+ public async Task LoadProductFriendlyNames(string productIds)
+ {
+ var result = "";
+
+ if (!await AuthorizeManageProducts())
+ return new JsonResult(new { Text = result });
+
+ if (string.IsNullOrWhiteSpace(productIds)) return new JsonResult(new { Text = result });
+ var ids = new List();
+ var rangeArray = productIds
+ .Split([','], StringSplitOptions.RemoveEmptyEntries)
+ .Select(x => x.Trim())
+ .ToList();
+
+ foreach (var str1 in rangeArray)
+ {
+ var str2 = str1;
+ if (str2.Contains(':'))
+ str2 = str2[..str2.IndexOf(":", StringComparison.Ordinal)];
+ ids.Add(str2);
+ }
+
+ var products = await _productService.GetProductsByIds(ids.ToArray(), true);
+ for (var i = 0; i <= products.Count - 1; i++)
+ {
+ result += products[i].Name;
+ if (i != products.Count - 1)
+ result += ", ";
+ }
+
+ return new JsonResult(new { Text = result });
+ }
+
+ private async Task AuthorizeManageDiscounts()
+ {
+ return await _permissionService.Authorize(StandardPermission.ManageDiscounts);
+ }
+
+ private async Task AuthorizeManageProducts()
+ {
+ return await _permissionService.Authorize(StandardPermission.ManageProducts);
+ }
+
+ private async Task GetDiscountById(string discountId)
+ {
+ return await _discountService.GetDiscountById(discountId);
+ }
+}
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/HasOneProductController.cs b/src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/HasOneProductController.cs
new file mode 100644
index 000000000..b707f3a2f
--- /dev/null
+++ b/src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/HasOneProductController.cs
@@ -0,0 +1,207 @@
+using DiscountRules.Standard.Models;
+using Grand.Business.Core.Interfaces.Catalog.Discounts;
+using Grand.Business.Core.Interfaces.Catalog.Products;
+using Grand.Business.Core.Interfaces.Common.Localization;
+using Grand.Business.Core.Interfaces.Common.Security;
+using Grand.Business.Core.Interfaces.Common.Stores;
+using Grand.Business.Core.Interfaces.Customers;
+using Grand.Domain.Catalog;
+using Grand.Domain.Discounts;
+using Grand.Domain.Permissions;
+using Grand.Infrastructure;
+using Grand.Web.Common.Controllers;
+using Grand.Web.Common.DataSource;
+using Grand.Web.Common.Localization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Rendering;
+
+namespace DiscountRules.Standard.Areas.Store.Controllers;
+
+public class HasOneProductController : BaseStorePluginController
+{
+ private readonly IDiscountService _discountService;
+ private readonly IPermissionService _permissionService;
+ private readonly IProductService _productService;
+ private readonly IStoreService _storeService;
+ private readonly ITranslationService _translationService;
+ private readonly IVendorService _vendorService;
+ private readonly IContextAccessor _contextAccessor;
+ private readonly IEnumTranslationService _enumTranslationService;
+
+ private string CurrentStoreId => _contextAccessor.WorkContext.CurrentCustomer.StaffStoreId;
+
+ public HasOneProductController(IDiscountService discountService,
+ IPermissionService permissionService,
+ IContextAccessor contextAccessor,
+ ITranslationService translationService,
+ IStoreService storeService,
+ IVendorService vendorService,
+ IProductService productService,
+ IEnumTranslationService enumTranslationService)
+ {
+ _discountService = discountService;
+ _permissionService = permissionService;
+ _contextAccessor = contextAccessor;
+ _translationService = translationService;
+ _storeService = storeService;
+ _vendorService = vendorService;
+ _productService = productService;
+ _enumTranslationService = enumTranslationService;
+ }
+
+ private async Task AuthorizeAsync(Permission permission)
+ {
+ if (!await _permissionService.Authorize(permission))
+ return Content("Access denied");
+ return null;
+ }
+
+ private async Task GetDiscountAsync(string discountId)
+ {
+ var discount = await _discountService.GetDiscountById(discountId);
+ if (discount == null)
+ throw new ArgumentException("Discount could not be loaded");
+ return discount;
+ }
+
+ public async Task Configure(string discountId, string discountRequirementId)
+ {
+ var authResult = await AuthorizeAsync(StandardPermission.ManageDiscounts);
+ if (authResult != null) return authResult;
+
+ var discount = await GetDiscountAsync(discountId);
+
+ var restrictedProductIds = string.Empty;
+ if (!string.IsNullOrEmpty(discountRequirementId))
+ {
+ var discountRequirement = discount.DiscountRules.FirstOrDefault(dr => dr.Id == discountRequirementId);
+ if (discountRequirement == null)
+ return Content("Failed to load requirement.");
+
+ restrictedProductIds = discountRequirement.Metadata;
+ }
+
+ var model = new RequirementOneProductModel {
+ RequirementId = !string.IsNullOrEmpty(discountRequirementId) ? discountRequirementId : "",
+ DiscountId = discountId,
+ Products = restrictedProductIds
+ };
+
+ //add a prefix
+ ViewData.TemplateInfo.HtmlFieldPrefix =
+ $"DiscountRulesHasOneProduct{discount.Id}-{(!string.IsNullOrEmpty(discountRequirementId) ? discountRequirementId : "")}";
+
+ return View(model);
+ }
+
+ [HttpPost]
+ [AutoValidateAntiforgeryToken]
+ public async Task Configure(string discountId, string discountRequirementId, string productIds)
+ {
+ var authResult = await AuthorizeAsync(StandardPermission.ManageDiscounts);
+ if (authResult != null) return authResult;
+
+ var discount = await GetDiscountAsync(discountId);
+
+ DiscountRule discountRequirement = null;
+ if (!string.IsNullOrEmpty(discountRequirementId))
+ discountRequirement = discount.DiscountRules.FirstOrDefault(dr => dr.Id == discountRequirementId);
+
+ if (discountRequirement != null)
+ {
+ //update existing rule
+ discountRequirement.Metadata = productIds;
+ await _discountService.UpdateDiscount(discount);
+ }
+ else
+ {
+ //save new rule
+ discountRequirement = new DiscountRule {
+ DiscountRequirementRuleSystemName = "DiscountRules.HasOneProduct",
+ Metadata = productIds
+ };
+ discount.DiscountRules.Add(discountRequirement);
+ await _discountService.UpdateDiscount(discount);
+ }
+
+ return new JsonResult(new { Result = true, NewRequirementId = discountRequirement.Id });
+ }
+
+ public async Task ProductAddPopup(string btnId, string productIdsInput)
+ {
+ var authResult = await AuthorizeAsync(StandardPermission.ManageProducts);
+ if (authResult != null) return authResult;
+
+ var model = new RequirementOneProductModel.AddProductModel();
+
+ //stores - scoped to current store manager's store
+ model.AvailableStores.Add(new SelectListItem { Text = _translationService.GetResource("Admin.Common.All"), Value = "" });
+ foreach (var s in await _storeService.GetAllStores())
+ model.AvailableStores.Add(new SelectListItem { Text = s.Shortcut, Value = s.Id });
+
+ //product types
+ model.AvailableProductTypes = _enumTranslationService.ToSelectList(ProductType.SimpleProduct, false).ToList();
+ model.AvailableProductTypes.Insert(0,
+ new SelectListItem { Text = _translationService.GetResource("Admin.Common.All"), Value = "" });
+
+ ViewBag.productIdsInput = productIdsInput;
+ ViewBag.btnId = btnId;
+
+ return View(model);
+ }
+
+ [HttpPost]
+ [AutoValidateAntiforgeryToken]
+ public async Task ProductAddPopupList(DataSourceRequest command,
+ RequirementOneProductModel.AddProductModel model)
+ {
+ var authResult = await AuthorizeAsync(StandardPermission.ManageProducts);
+ if (authResult != null) return authResult;
+
+ var searchCategoryIds = new List();
+ if (!string.IsNullOrEmpty(model.SearchCategoryId))
+ searchCategoryIds.Add(model.SearchCategoryId);
+
+ var products = (await _productService.SearchProducts(
+ categoryIds: searchCategoryIds,
+ collectionId: model.SearchCollectionId,
+ storeId: !string.IsNullOrEmpty(model.SearchStoreId) ? model.SearchStoreId : CurrentStoreId,
+ productType: model.SearchProductTypeId > 0 ? (ProductType?)model.SearchProductTypeId : null,
+ keywords: model.SearchProductName,
+ pageIndex: command.Page - 1,
+ pageSize: command.PageSize,
+ showHidden: true
+ )).products;
+ var gridModel = new DataSourceResult {
+ Data = products.Select(x => new RequirementOneProductModel.ProductModel {
+ Id = x.Id,
+ Name = x.Name,
+ Published = x.Published
+ }),
+ Total = products.TotalCount
+ };
+
+ return new JsonResult(gridModel);
+ }
+
+ [HttpPost]
+ [AutoValidateAntiforgeryToken]
+ public async Task LoadProductFriendlyNames(string productIds)
+ {
+ var authResult = await AuthorizeAsync(StandardPermission.ManageProducts);
+ if (authResult != null) return authResult;
+
+ var result = "";
+
+ if (string.IsNullOrWhiteSpace(productIds)) return new JsonResult(new { Text = result });
+ var ids = productIds
+ .Split(',', StringSplitOptions.RemoveEmptyEntries)
+ .Select(x => x.Split(':')[0].Trim())
+ .ToList();
+
+ var products = await _productService.GetProductsByIds(ids.ToArray(), true);
+ result = string.Join(", ", products.Select(p => p.Name));
+
+ return new JsonResult(new { Text = result });
+ }
+}
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/ShoppingCartAmountController.cs b/src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/ShoppingCartAmountController.cs
new file mode 100644
index 000000000..9875942d8
--- /dev/null
+++ b/src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/ShoppingCartAmountController.cs
@@ -0,0 +1,89 @@
+using DiscountRules.Standard.Models;
+using Grand.Business.Core.Interfaces.Catalog.Discounts;
+using Grand.Business.Core.Interfaces.Common.Security;
+using Grand.Domain.Permissions;
+using Grand.Domain.Discounts;
+using Grand.Web.Common.Controllers;
+using Microsoft.AspNetCore.Mvc;
+using System.Globalization;
+
+namespace DiscountRules.Standard.Areas.Store.Controllers;
+
+public class ShoppingCartAmountController : BaseStorePluginController
+{
+ private readonly IDiscountService _discountService;
+ private readonly IPermissionService _permissionService;
+
+ public ShoppingCartAmountController(IDiscountService discountService,
+ IPermissionService permissionService)
+ {
+ _discountService = discountService;
+ _permissionService = permissionService;
+ }
+
+ public async Task Configure(string discountId, string discountRequirementId)
+ {
+ if (!await _permissionService.Authorize(StandardPermission.ManageDiscounts))
+ return Content("Access denied");
+
+ var discount = await _discountService.GetDiscountById(discountId);
+ if (discount == null)
+ throw new ArgumentException("Discount could not be loaded");
+
+ double spentAmountRequirement = 0;
+ if (!string.IsNullOrEmpty(discountRequirementId))
+ {
+ var discountRequirement = discount.DiscountRules.FirstOrDefault(dr => dr.Id == discountRequirementId);
+ if (discountRequirement == null)
+ return Content("Failed to load requirement.");
+
+ spentAmountRequirement = Convert.ToDouble(discountRequirement.Metadata);
+ }
+
+ var model = new RequirementShoppingCartModel {
+ RequirementId = !string.IsNullOrEmpty(discountRequirementId) ? discountRequirementId : "",
+ DiscountId = discountId,
+ SpentAmount = spentAmountRequirement
+ };
+
+ //add a prefix
+ ViewData.TemplateInfo.HtmlFieldPrefix =
+ $"DiscountRulesShoppingCart{(!string.IsNullOrEmpty(discountRequirementId) ? discountRequirementId : "")}";
+
+ return View(model);
+ }
+
+ [HttpPost]
+ public async Task Configure(string discountId, string discountRequirementId, double spentAmount)
+ {
+ if (!await _permissionService.Authorize(StandardPermission.ManageDiscounts))
+ return Content("Access denied");
+
+ var discount = await _discountService.GetDiscountById(discountId);
+ if (discount == null)
+ throw new ArgumentException("Discount could not be loaded");
+
+ DiscountRule discountRequirement = null;
+ if (!string.IsNullOrEmpty(discountRequirementId))
+ discountRequirement = discount.DiscountRules.FirstOrDefault(dr => dr.Id == discountRequirementId);
+
+ if (discountRequirement != null)
+ {
+ //update existing rule
+ discountRequirement.Metadata = spentAmount.ToString(CultureInfo.InvariantCulture);
+ await _discountService.UpdateDiscount(discount);
+ }
+ else
+ {
+ //save new rule
+ discountRequirement = new DiscountRule {
+ DiscountRequirementRuleSystemName = "DiscountRequirement.ShoppingCart",
+ Metadata = spentAmount.ToString(CultureInfo.InvariantCulture)
+ };
+ discount.DiscountRules.Add(discountRequirement);
+ await _discountService.UpdateDiscount(discount);
+ }
+
+ return Json(new { Result = true, NewRequirementId = discountRequirement.Id });
+ }
+}
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Store/Views/CustomerGroups/Configure.cshtml b/src/Plugins/DiscountRules.Standard/Areas/Store/Views/CustomerGroups/Configure.cshtml
new file mode 100644
index 000000000..83a0efbc7
--- /dev/null
+++ b/src/Plugins/DiscountRules.Standard/Areas/Store/Views/CustomerGroups/Configure.cshtml
@@ -0,0 +1,54 @@
+@{
+ Layout = "";
+}
+@using System.Text.Encodings.Web
+@model DiscountRules.Standard.Models.RequirementCustomerGroupsModel
+
+
+
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Store/Views/HadSpentAmount/Configure.cshtml b/src/Plugins/DiscountRules.Standard/Areas/Store/Views/HadSpentAmount/Configure.cshtml
new file mode 100644
index 000000000..747f4c264
--- /dev/null
+++ b/src/Plugins/DiscountRules.Standard/Areas/Store/Views/HadSpentAmount/Configure.cshtml
@@ -0,0 +1,53 @@
+@{
+ Layout = "";
+}
+@using System.Text.Encodings.Web
+@model DiscountRules.Standard.Models.RequirementSpentAmountModel
+
+
+
+
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Store/Views/HasAllProducts/Configure.cshtml b/src/Plugins/DiscountRules.Standard/Areas/Store/Views/HasAllProducts/Configure.cshtml
new file mode 100644
index 000000000..bf1a881b4
--- /dev/null
+++ b/src/Plugins/DiscountRules.Standard/Areas/Store/Views/HasAllProducts/Configure.cshtml
@@ -0,0 +1,126 @@
+@{
+ Layout = "";
+}
+@using System.Text.Encodings.Web
+@model DiscountRules.Standard.Models.RequirementAllProductsModel
+
+
+
+
+
+
+
+
+
+
@Loc["admin.marketing.Discounts.Requirements.Saved"]
+
+
@Loc["Common.Wait..."]
+
+
+
+
+
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Store/Views/HasAllProducts/ProductAddPopup.cshtml b/src/Plugins/DiscountRules.Standard/Areas/Store/Views/HasAllProducts/ProductAddPopup.cshtml
new file mode 100644
index 000000000..b4df0f4e2
--- /dev/null
+++ b/src/Plugins/DiscountRules.Standard/Areas/Store/Views/HasAllProducts/ProductAddPopup.cshtml
@@ -0,0 +1,193 @@
+@{
+ Layout = "";
+}
+@using Grand.Domain.Common
+@model DiscountRules.Standard.Models.RequirementAllProductsModel.AddProductModel
+@inject AdminAreaSettings adminAreaSettings
+@{
+ //page title
+ ViewBag.Title = Loc["Plugins.DiscountRules.HasAllProducts.Fields.Products.Choose"];
+}
+
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Store/Views/HasOneProduct/Configure.cshtml b/src/Plugins/DiscountRules.Standard/Areas/Store/Views/HasOneProduct/Configure.cshtml
new file mode 100644
index 000000000..b98eb9e8e
--- /dev/null
+++ b/src/Plugins/DiscountRules.Standard/Areas/Store/Views/HasOneProduct/Configure.cshtml
@@ -0,0 +1,125 @@
+@{
+ Layout = "";
+}
+@using System.Text.Encodings.Web
+@model DiscountRules.Standard.Models.RequirementOneProductModel
+
+
+
+
+
+
+
+
+
+
+
@Loc["admin.marketing.Discounts.Requirements.Saved"]
+
@Loc["Common.Wait..."]
+
+
+
+
+
+
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Store/Views/HasOneProduct/ProductAddPopup.cshtml b/src/Plugins/DiscountRules.Standard/Areas/Store/Views/HasOneProduct/ProductAddPopup.cshtml
new file mode 100644
index 000000000..1ff88101d
--- /dev/null
+++ b/src/Plugins/DiscountRules.Standard/Areas/Store/Views/HasOneProduct/ProductAddPopup.cshtml
@@ -0,0 +1,192 @@
+@{
+ Layout = "";
+}
+@using Grand.Domain.Common
+@model DiscountRules.Standard.Models.RequirementOneProductModel.AddProductModel
+@inject AdminAreaSettings adminAreaSettings
+@{
+ //page title
+ ViewBag.Title = Loc["Plugins.DiscountRules.HasOneProduct.Fields.Products.Choose"];
+}
+
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Store/Views/ShoppingCartAmount/Configure.cshtml b/src/Plugins/DiscountRules.Standard/Areas/Store/Views/ShoppingCartAmount/Configure.cshtml
new file mode 100644
index 000000000..fe3d8f5dc
--- /dev/null
+++ b/src/Plugins/DiscountRules.Standard/Areas/Store/Views/ShoppingCartAmount/Configure.cshtml
@@ -0,0 +1,45 @@
+@{
+ Layout = "";
+}
+@using System.Text.Encodings.Web
+@model DiscountRules.Standard.Models.RequirementShoppingCartModel
+
+
+
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Store/Views/_ViewImports.cshtml b/src/Plugins/DiscountRules.Standard/Areas/Store/Views/_ViewImports.cshtml
new file mode 100644
index 000000000..e02b25523
--- /dev/null
+++ b/src/Plugins/DiscountRules.Standard/Areas/Store/Views/_ViewImports.cshtml
@@ -0,0 +1,11 @@
+@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
+@addTagHelper *, Grand.Web.Common
+@addTagHelper *, Grand.Web
+
+@using Microsoft.AspNetCore.Mvc.ViewFeatures
+@using Grand.Infrastructure.Extensions
+@using Grand.Web.Common
+@using Grand.Web.Common.Localization
+@using Grand.Web.Common.Page
+
+@inject LocService Loc
diff --git a/src/Web/Grand.Web.Common/Controllers/BaseStorePluginController.cs b/src/Web/Grand.Web.Common/Controllers/BaseStorePluginController.cs
new file mode 100644
index 000000000..358098058
--- /dev/null
+++ b/src/Web/Grand.Web.Common/Controllers/BaseStorePluginController.cs
@@ -0,0 +1,12 @@
+using Grand.Web.Common.Filters;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Grand.Web.Common.Controllers;
+
+///
+/// Base controller for plugins in the Store area
+///
+[AuthorizeStore]
+[Area("Store")]
+[AuthorizeMenu]
+public abstract class BaseStorePluginController : BaseController;
diff --git a/src/Web/Grand.Web.Store/Controllers/DiscountController.cs b/src/Web/Grand.Web.Store/Controllers/DiscountController.cs
index f2294ba7f..58bb2fca9 100644
--- a/src/Web/Grand.Web.Store/Controllers/DiscountController.cs
+++ b/src/Web/Grand.Web.Store/Controllers/DiscountController.cs
@@ -6,6 +6,7 @@
using Grand.Business.Core.Interfaces.Common.Directory;
using Grand.Business.Core.Interfaces.Common.Localization;
using Grand.Business.Core.Queries.Catalog;
+using Grand.Domain.Discounts;
using Grand.Domain.Permissions;
using Grand.Infrastructure;
using Grand.Web.AdminShared.Extensions;
@@ -60,6 +61,15 @@ public DiscountController(
private string CurrentStoreId =>
_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId;
+ private string BuildStoreRequirementUrl(IDiscountRule discountRequirementRule, Discount discount, string discountRequirementId)
+ {
+ var storeLocation = _contextAccessor.StoreContext.CurrentHost.Url.TrimEnd('/');
+ var configUrl = discountRequirementRule.GetConfigurationUrl(discount.Id, discountRequirementId);
+ if (configUrl.StartsWith("Admin/", StringComparison.OrdinalIgnoreCase))
+ configUrl = "Store/" + configUrl[6..];
+ return $"{storeLocation}/{configUrl}";
+ }
+
#endregion
#region Discounts
@@ -297,8 +307,7 @@ public async Task GetDiscountRequirementConfigurationUrl(string r
var singleRequirement = discountPlugin.GetRequirementRules().FirstOrDefault(x =>
x.SystemName.Equals(rulesystemName, StringComparison.OrdinalIgnoreCase));
- var url = _discountViewModelService.GetRequirementUrlInternal(singleRequirement, discount,
- discountRequirementId);
+ var url = BuildStoreRequirementUrl(singleRequirement, discount, discountRequirementId);
return Json(new { url });
}
@@ -325,8 +334,7 @@ public async Task GetDiscountRequirementMetaInfo(string discountR
var discountRequirementRule = discountPlugin.GetRequirementRules()
.First(x => x.SystemName == discountRequirement.DiscountRequirementRuleSystemName);
- var url = _discountViewModelService.GetRequirementUrlInternal(discountRequirementRule, discount,
- discountRequirementId);
+ var url = BuildStoreRequirementUrl(discountRequirementRule, discount, discountRequirementId);
var ruleName = discountRequirementRule.FriendlyName;
return Json(new { url, ruleName });
From 1ea235235267ac87678015cf4e694befc789cfd9 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 31 May 2026 19:27:00 +0000
Subject: [PATCH 04/16] Fix code review issues: consolidate ready blocks and
fix formatting
Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com>
---
.../Areas/Store/Views/CustomerGroups/Configure.cshtml | 2 +-
.../Areas/Store/Views/HasAllProducts/Configure.cshtml | 6 ++----
2 files changed, 3 insertions(+), 5 deletions(-)
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Store/Views/CustomerGroups/Configure.cshtml b/src/Plugins/DiscountRules.Standard/Areas/Store/Views/CustomerGroups/Configure.cshtml
index 83a0efbc7..3e2439120 100644
--- a/src/Plugins/DiscountRules.Standard/Areas/Store/Views/CustomerGroups/Configure.cshtml
+++ b/src/Plugins/DiscountRules.Standard/Areas/Store/Views/CustomerGroups/Configure.cshtml
@@ -11,7 +11,7 @@
var discountId = '@Model.DiscountId';
var requirementId = '@Model.RequirementId';
- if(customerGroupId == "")return;
+ if (customerGroupId == "") return;
var postData = {
discountId: discountId,
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Store/Views/HasAllProducts/Configure.cshtml b/src/Plugins/DiscountRules.Standard/Areas/Store/Views/HasAllProducts/Configure.cshtml
index bf1a881b4..ba45cd06b 100644
--- a/src/Plugins/DiscountRules.Standard/Areas/Store/Views/HasAllProducts/Configure.cshtml
+++ b/src/Plugins/DiscountRules.Standard/Areas/Store/Views/HasAllProducts/Configure.cshtml
@@ -77,12 +77,9 @@
//return false to don't reload a page
return false;
});
- });
- $(document).ready(function () {
loadDiscountRequirementProductFriendlyNames@(Model.RequirementId)();
- });
- $(document).ready(function () {
+
$('#@Html.IdFor(model => model.Products)')
.data('timeout', null)
.keyup(function() {
@@ -91,6 +88,7 @@
$(this).data('timeout', setTimeout(loadDiscountRequirementProductFriendlyNames@(Model.RequirementId), 1000));
});
});
+
function loadDiscountRequirementProductFriendlyNames@(Model.RequirementId)() {
var inputValue = $('#@Html.IdFor(model => model.Products)').val();
if (inputValue) {
From e1179b71ed2262937b2814d846366e8623e11e6d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 1 Jun 2026 17:54:18 +0000
Subject: [PATCH 05/16] Refactor discount requirement URL logic - avoid plugin
code duplication
Instead of duplicating all DiscountRules.Standard plugin controllers/views
into a Store area, this commit takes a DRY approach:
- Add optional `area` parameter to IDiscountRule.GetConfigurationUrl
- All 5 providers use the area param to build area-aware URLs
- Store DiscountController calls GetConfigurationUrl(..., "Store") directly,
no more Admin/->Store/ string replacement
- New BaseDiscountRulePluginController: bypasses [AuthorizeAdmin] via
ignore:true, so store managers can reach the same Admin-area controllers
- Existing Admin controllers now handle both Admin/* and Store/* routes
via dual [Route] attributes
- Store-manager context (CurrentStoreId) scopes product search in
HasOneProduct/HasAllProducts when accessed from the Store area
- Delete all Store-area plugin controller/view duplicates
- Delete BaseStorePluginController (no longer needed)
Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com>
---
.../Catalog/Discounts/IDiscountRule.cs | 3 +-
.../Controllers/CustomerGroupsController.cs | 8 +-
.../Controllers/HadSpentAmountController.cs | 8 +-
.../Controllers/HasAllProductsController.cs | 41 ++--
.../Controllers/HasOneProductController.cs | 38 ++-
.../ShoppingCartAmountController.cs | 8 +-
.../Controllers/CustomerGroupsController.cs | 101 --------
.../Controllers/HadSpentAmountController.cs | 91 -------
.../Controllers/HasAllProductsController.cs | 227 ------------------
.../Controllers/HasOneProductController.cs | 207 ----------------
.../ShoppingCartAmountController.cs | 89 -------
.../Views/CustomerGroups/Configure.cshtml | 54 -----
.../Views/HadSpentAmount/Configure.cshtml | 53 ----
.../Views/HasAllProducts/Configure.cshtml | 124 ----------
.../HasAllProducts/ProductAddPopup.cshtml | 193 ---------------
.../Views/HasOneProduct/Configure.cshtml | 125 ----------
.../HasOneProduct/ProductAddPopup.cshtml | 192 ---------------
.../Views/ShoppingCartAmount/Configure.cshtml | 45 ----
.../Areas/Store/Views/_ViewImports.cshtml | 11 -
.../BaseDiscountRulePluginController.cs | 16 ++
.../Providers/CustomerGroupDiscountRule.cs | 4 +-
.../Providers/HadSpentAmountDiscountRule.cs | 4 +-
.../Providers/HasAllProductsDiscountRule.cs | 4 +-
.../Providers/HasOneProductDiscountRule.cs | 4 +-
.../Providers/ShoppingCartDiscountRule.cs | 4 +-
.../Discounts/DiscountProviderTest.cs | 2 +-
.../Controllers/BaseStorePluginController.cs | 12 -
.../Controllers/DiscountController.cs | 4 +-
28 files changed, 98 insertions(+), 1574 deletions(-)
delete mode 100644 src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/CustomerGroupsController.cs
delete mode 100644 src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/HadSpentAmountController.cs
delete mode 100644 src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/HasAllProductsController.cs
delete mode 100644 src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/HasOneProductController.cs
delete mode 100644 src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/ShoppingCartAmountController.cs
delete mode 100644 src/Plugins/DiscountRules.Standard/Areas/Store/Views/CustomerGroups/Configure.cshtml
delete mode 100644 src/Plugins/DiscountRules.Standard/Areas/Store/Views/HadSpentAmount/Configure.cshtml
delete mode 100644 src/Plugins/DiscountRules.Standard/Areas/Store/Views/HasAllProducts/Configure.cshtml
delete mode 100644 src/Plugins/DiscountRules.Standard/Areas/Store/Views/HasAllProducts/ProductAddPopup.cshtml
delete mode 100644 src/Plugins/DiscountRules.Standard/Areas/Store/Views/HasOneProduct/Configure.cshtml
delete mode 100644 src/Plugins/DiscountRules.Standard/Areas/Store/Views/HasOneProduct/ProductAddPopup.cshtml
delete mode 100644 src/Plugins/DiscountRules.Standard/Areas/Store/Views/ShoppingCartAmount/Configure.cshtml
delete mode 100644 src/Plugins/DiscountRules.Standard/Areas/Store/Views/_ViewImports.cshtml
create mode 100644 src/Plugins/DiscountRules.Standard/Controllers/BaseDiscountRulePluginController.cs
delete mode 100644 src/Web/Grand.Web.Common/Controllers/BaseStorePluginController.cs
diff --git a/src/Business/Grand.Business.Core/Interfaces/Catalog/Discounts/IDiscountRule.cs b/src/Business/Grand.Business.Core/Interfaces/Catalog/Discounts/IDiscountRule.cs
index 512309733..30b54022e 100644
--- a/src/Business/Grand.Business.Core/Interfaces/Catalog/Discounts/IDiscountRule.cs
+++ b/src/Business/Grand.Business.Core/Interfaces/Catalog/Discounts/IDiscountRule.cs
@@ -28,6 +28,7 @@ public interface IDiscountRule
///
/// Discount id
/// Discount requirement id
+ /// Area name (e.g. "Admin" or "Store")
/// URL
- string GetConfigurationUrl(string discountId, string discountRequirementId);
+ string GetConfigurationUrl(string discountId, string discountRequirementId, string area = "Admin");
}
\ No newline at end of file
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/CustomerGroupsController.cs b/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/CustomerGroupsController.cs
index 375e4362a..1e1c2b980 100644
--- a/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/CustomerGroupsController.cs
+++ b/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/CustomerGroupsController.cs
@@ -1,16 +1,18 @@
-using DiscountRules.Standard.Models;
+using DiscountRules.Standard.Controllers;
+using DiscountRules.Standard.Models;
using Grand.Business.Core.Interfaces.Catalog.Discounts;
using Grand.Business.Core.Interfaces.Common.Directory;
using Grand.Business.Core.Interfaces.Common.Security;
using Grand.Domain.Permissions;
using Grand.Domain.Discounts;
-using Grand.Web.Common.Controllers;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace DiscountRules.Standard.Areas.Admin.Controllers;
-public class CustomerGroupsController : BaseAdminPluginController
+[Route("Admin/CustomerGroups/[action]")]
+[Route("Store/CustomerGroups/[action]")]
+public class CustomerGroupsController : BaseDiscountRulePluginController
{
private readonly IDiscountService _discountService;
private readonly IGroupService _groupService;
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/HadSpentAmountController.cs b/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/HadSpentAmountController.cs
index 2a0372642..a2926761d 100644
--- a/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/HadSpentAmountController.cs
+++ b/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/HadSpentAmountController.cs
@@ -1,15 +1,17 @@
-using DiscountRules.Standard.Models;
+using DiscountRules.Standard.Controllers;
+using DiscountRules.Standard.Models;
using Grand.Business.Core.Interfaces.Catalog.Discounts;
using Grand.Business.Core.Interfaces.Common.Security;
using Grand.Domain.Permissions;
using Grand.Domain.Discounts;
-using Grand.Web.Common.Controllers;
using Microsoft.AspNetCore.Mvc;
using System.Globalization;
namespace DiscountRules.Standard.Areas.Admin.Controllers;
-public class HadSpentAmountController : BaseAdminPluginController
+[Route("Admin/HadSpentAmount/[action]")]
+[Route("Store/HadSpentAmount/[action]")]
+public class HadSpentAmountController : BaseDiscountRulePluginController
{
private readonly IDiscountService _discountService;
private readonly IPermissionService _permissionService;
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/HasAllProductsController.cs b/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/HasAllProductsController.cs
index 0d7f5fc4f..3241722b3 100644
--- a/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/HasAllProductsController.cs
+++ b/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/HasAllProductsController.cs
@@ -1,4 +1,5 @@
-using DiscountRules.Standard.Models;
+using DiscountRules.Standard.Controllers;
+using DiscountRules.Standard.Models;
using Grand.Business.Core.Interfaces.Catalog.Discounts;
using Grand.Business.Core.Interfaces.Catalog.Products;
using Grand.Business.Core.Interfaces.Common.Localization;
@@ -9,7 +10,6 @@
using Grand.Domain.Discounts;
using Grand.Domain.Permissions;
using Grand.Infrastructure;
-using Grand.Web.Common.Controllers;
using Grand.Web.Common.DataSource;
using Grand.Web.Common.Localization;
using Microsoft.AspNetCore.Mvc;
@@ -17,7 +17,9 @@
namespace DiscountRules.Standard.Areas.Admin.Controllers;
-public class HasAllProductsController : BaseAdminPluginController
+[Route("Admin/HasAllProducts/[action]")]
+[Route("Store/HasAllProducts/[action]")]
+public class HasAllProductsController : BaseDiscountRulePluginController
{
private readonly IDiscountService _discountService;
private readonly IPermissionService _permissionService;
@@ -28,7 +30,7 @@ public class HasAllProductsController : BaseAdminPluginController
private readonly IContextAccessor _contextAccessor;
private readonly IEnumTranslationService _enumTranslationService;
-
+ private string CurrentStoreId => _contextAccessor.WorkContext.CurrentCustomer.StaffStoreId;
public HasAllProductsController(IDiscountService discountService,
IPermissionService permissionService,
IContextAccessor contextAccessor,
@@ -127,22 +129,30 @@ public async Task ProductAddPopup(string btnId, string productIds
IsLoggedInAsVendor = _contextAccessor.WorkContext.CurrentVendor != null
};
- //stores
- model.AvailableStores.Add(new SelectListItem { Text = _translationService.GetResource("Admin.Common.All"), Value = "" });
- foreach (var s in await _storeService.GetAllStores())
- model.AvailableStores.Add(new SelectListItem { Text = s.Shortcut, Value = s.Id });
-
- //vendors
- model.AvailableVendors.Add(new SelectListItem { Text = _translationService.GetResource("Admin.Common.All"), Value = "" });
- foreach (var v in await _vendorService.GetAllVendors(showHidden: true))
- model.AvailableVendors.Add(new SelectListItem { Text = v.Name, Value = v.Id });
+ //stores - when acting as a store manager, scope to their store
+ if (!string.IsNullOrEmpty(CurrentStoreId))
+ {
+ var store = await _storeService.GetStoreById(CurrentStoreId);
+ if (store != null)
+ model.AvailableStores.Add(new SelectListItem { Text = store.Shortcut, Value = store.Id });
+ }
+ else
+ {
+ model.AvailableStores.Add(new SelectListItem { Text = _translationService.GetResource("Admin.Common.All"), Value = "" });
+ foreach (var s in await _storeService.GetAllStores())
+ model.AvailableStores.Add(new SelectListItem { Text = s.Shortcut, Value = s.Id });
+
+ //vendors (only shown to admins)
+ model.AvailableVendors.Add(new SelectListItem { Text = _translationService.GetResource("Admin.Common.All"), Value = "" });
+ foreach (var v in await _vendorService.GetAllVendors(showHidden: true))
+ model.AvailableVendors.Add(new SelectListItem { Text = v.Name, Value = v.Id });
+ }
//product types
model.AvailableProductTypes = _enumTranslationService.ToSelectList(ProductType.SimpleProduct, false).ToList();
model.AvailableProductTypes.Insert(0,
new SelectListItem { Text = _translationService.GetResource("Admin.Common.All"), Value = "" });
-
ViewBag.productIdsInput = productIdsInput;
ViewBag.btnId = btnId;
@@ -159,6 +169,9 @@ public async Task ProductAddPopupList(DataSourceRequest command,
//a vendor should have access only to his products
if (_contextAccessor.WorkContext.CurrentVendor != null) model.SearchVendorId = _contextAccessor.WorkContext.CurrentVendor.Id;
+ //a store manager should only search within their store
+ if (!string.IsNullOrEmpty(CurrentStoreId)) model.SearchStoreId = CurrentStoreId;
+
var searchCategoryIds = new List();
if (!string.IsNullOrEmpty(model.SearchCategoryId))
searchCategoryIds.Add(model.SearchCategoryId);
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/HasOneProductController.cs b/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/HasOneProductController.cs
index 2c4d74dd7..e8c5127f9 100644
--- a/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/HasOneProductController.cs
+++ b/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/HasOneProductController.cs
@@ -1,4 +1,5 @@
-using DiscountRules.Standard.Models;
+using DiscountRules.Standard.Controllers;
+using DiscountRules.Standard.Models;
using Grand.Business.Core.Interfaces.Catalog.Discounts;
using Grand.Business.Core.Interfaces.Catalog.Products;
using Grand.Business.Core.Interfaces.Common.Localization;
@@ -10,7 +11,6 @@
using Grand.Domain.Permissions;
using Grand.Domain.Vendors;
using Grand.Infrastructure;
-using Grand.Web.Common.Controllers;
using Grand.Web.Common.DataSource;
using Grand.Web.Common.Localization;
using Microsoft.AspNetCore.Mvc;
@@ -18,7 +18,9 @@
namespace DiscountRules.Standard.Areas.Admin.Controllers;
-public class HasOneProductController : BaseAdminPluginController
+[Route("Admin/HasOneProduct/[action]")]
+[Route("Store/HasOneProduct/[action]")]
+public class HasOneProductController : BaseDiscountRulePluginController
{
private readonly IDiscountService _discountService;
private readonly IPermissionService _permissionService;
@@ -30,6 +32,7 @@ public class HasOneProductController : BaseAdminPluginController
private readonly IEnumTranslationService _enumTranslationService;
private Vendor CurrentVendor => _contextAccessor.WorkContext.CurrentVendor;
+ private string CurrentStoreId => _contextAccessor.WorkContext.CurrentCustomer.StaffStoreId;
public HasOneProductController(IDiscountService discountService,
IPermissionService permissionService,
@@ -138,15 +141,24 @@ public async Task ProductAddPopup(string btnId, string productIds
IsLoggedInAsVendor = CurrentVendor != null
};
- //stores
- model.AvailableStores.Add(new SelectListItem { Text = _translationService.GetResource("Admin.Common.All"), Value = "" });
- foreach (var s in await _storeService.GetAllStores())
- model.AvailableStores.Add(new SelectListItem { Text = s.Shortcut, Value = s.Id });
-
- //vendors
- model.AvailableVendors.Add(new SelectListItem { Text = _translationService.GetResource("Admin.Common.All"), Value = "" });
- foreach (var v in await _vendorService.GetAllVendors(showHidden: true))
- model.AvailableVendors.Add(new SelectListItem { Text = v.Name, Value = v.Id });
+ //stores - when acting as a store manager, scope to their store
+ if (!string.IsNullOrEmpty(CurrentStoreId))
+ {
+ var store = await _storeService.GetStoreById(CurrentStoreId);
+ if (store != null)
+ model.AvailableStores.Add(new SelectListItem { Text = store.Shortcut, Value = store.Id });
+ }
+ else
+ {
+ model.AvailableStores.Add(new SelectListItem { Text = _translationService.GetResource("Admin.Common.All"), Value = "" });
+ foreach (var s in await _storeService.GetAllStores())
+ model.AvailableStores.Add(new SelectListItem { Text = s.Shortcut, Value = s.Id });
+
+ //vendors (only shown to admins)
+ model.AvailableVendors.Add(new SelectListItem { Text = _translationService.GetResource("Admin.Common.All"), Value = "" });
+ foreach (var v in await _vendorService.GetAllVendors(showHidden: true))
+ model.AvailableVendors.Add(new SelectListItem { Text = v.Name, Value = v.Id });
+ }
//product types
model.AvailableProductTypes = _enumTranslationService.ToSelectList(ProductType.SimpleProduct, false).ToList();
@@ -169,6 +181,8 @@ public async Task ProductAddPopupList(DataSourceRequest command,
//a vendor should have access only to his products
if (CurrentVendor != null) model.SearchVendorId = CurrentVendor.Id;
+ //a store manager should only search within their store
+ if (!string.IsNullOrEmpty(CurrentStoreId)) model.SearchStoreId = CurrentStoreId;
var searchCategoryIds = new List();
if (!string.IsNullOrEmpty(model.SearchCategoryId))
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/ShoppingCartAmountController.cs b/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/ShoppingCartAmountController.cs
index 99d1b911f..18421a45a 100644
--- a/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/ShoppingCartAmountController.cs
+++ b/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/ShoppingCartAmountController.cs
@@ -1,15 +1,17 @@
-using DiscountRules.Standard.Models;
+using DiscountRules.Standard.Controllers;
+using DiscountRules.Standard.Models;
using Grand.Business.Core.Interfaces.Catalog.Discounts;
using Grand.Business.Core.Interfaces.Common.Security;
using Grand.Domain.Permissions;
using Grand.Domain.Discounts;
-using Grand.Web.Common.Controllers;
using Microsoft.AspNetCore.Mvc;
using System.Globalization;
namespace DiscountRules.Standard.Areas.Admin.Controllers;
-public class ShoppingCartAmountController : BaseAdminPluginController
+[Route("Admin/ShoppingCartAmount/[action]")]
+[Route("Store/ShoppingCartAmount/[action]")]
+public class ShoppingCartAmountController : BaseDiscountRulePluginController
{
private readonly IDiscountService _discountService;
private readonly IPermissionService _permissionService;
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/CustomerGroupsController.cs b/src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/CustomerGroupsController.cs
deleted file mode 100644
index ebeaf8c3a..000000000
--- a/src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/CustomerGroupsController.cs
+++ /dev/null
@@ -1,101 +0,0 @@
-using DiscountRules.Standard.Models;
-using Grand.Business.Core.Interfaces.Catalog.Discounts;
-using Grand.Business.Core.Interfaces.Common.Directory;
-using Grand.Business.Core.Interfaces.Common.Security;
-using Grand.Domain.Permissions;
-using Grand.Domain.Discounts;
-using Grand.Web.Common.Controllers;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.Rendering;
-
-namespace DiscountRules.Standard.Areas.Store.Controllers;
-
-public class CustomerGroupsController : BaseStorePluginController
-{
- private readonly IDiscountService _discountService;
- private readonly IGroupService _groupService;
- private readonly IPermissionService _permissionService;
-
- public CustomerGroupsController(
- IDiscountService discountService,
- IGroupService groupService,
- IPermissionService permissionService)
- {
- _discountService = discountService;
- _groupService = groupService;
- _permissionService = permissionService;
- }
-
- public async Task Configure(string discountId, string discountRequirementId)
- {
- if (!await _permissionService.Authorize(StandardPermission.ManageDiscounts))
- return Content("Access denied");
-
- var discount = await _discountService.GetDiscountById(discountId);
- if (discount == null)
- throw new ArgumentException("Discount could not be loaded");
-
- DiscountRule discountRequirement = null;
- if (!string.IsNullOrEmpty(discountRequirementId))
- {
- discountRequirement = discount.DiscountRules.FirstOrDefault(dr => dr.Id == discountRequirementId);
- if (discountRequirement == null)
- return Content("Failed to load requirement.");
- }
-
- var model = new RequirementCustomerGroupsModel {
- RequirementId = !string.IsNullOrEmpty(discountRequirementId) ? discountRequirementId : "",
- DiscountId = discountId,
- CustomerGroupId = discountRequirement?.Metadata
- };
-
- //customer groups
- model.AvailableCustomerGroups.Add(new SelectListItem { Text = "Select customer group", Value = "" });
- foreach (var cr in await _groupService.GetAllCustomerGroups(showHidden: true))
- model.AvailableCustomerGroups.Add(new SelectListItem {
- Text = cr.Name, Value = cr.Id,
- Selected = discountRequirement != null && cr.Id == discountRequirement.Metadata
- });
-
- //add a prefix
- ViewData.TemplateInfo.HtmlFieldPrefix =
- $"DiscountRulesCustomerGroups{(!string.IsNullOrEmpty(discountRequirementId) ? discountRequirementId : "")}";
-
- return View(model);
- }
-
- [HttpPost]
- [AutoValidateAntiforgeryToken]
- public async Task Configure(string discountId, string discountRequirementId, string customerGroupId)
- {
- if (!await _permissionService.Authorize(StandardPermission.ManageDiscounts))
- return Content("Access denied");
-
- var discount = await _discountService.GetDiscountById(discountId);
- if (discount == null)
- throw new ArgumentException("Discount could not be loaded");
-
- DiscountRule discountRequirement = null;
- if (!string.IsNullOrEmpty(discountRequirementId))
- discountRequirement = discount.DiscountRules.FirstOrDefault(dr => dr.Id == discountRequirementId);
-
- if (discountRequirement != null)
- {
- //update existing rule
- discountRequirement.Metadata = customerGroupId;
- await _discountService.UpdateDiscount(discount);
- }
- else
- {
- //save new rule
- discountRequirement = new DiscountRule {
- DiscountRequirementRuleSystemName = "DiscountRules.Standard.MustBeAssignedToCustomerGroup",
- Metadata = customerGroupId
- };
- discount.DiscountRules.Add(discountRequirement);
- await _discountService.UpdateDiscount(discount);
- }
-
- return Json(new { Result = true, NewRequirementId = discountRequirement.Id });
- }
-}
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/HadSpentAmountController.cs b/src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/HadSpentAmountController.cs
deleted file mode 100644
index c03bdd7d6..000000000
--- a/src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/HadSpentAmountController.cs
+++ /dev/null
@@ -1,91 +0,0 @@
-using DiscountRules.Standard.Models;
-using Grand.Business.Core.Interfaces.Catalog.Discounts;
-using Grand.Business.Core.Interfaces.Common.Security;
-using Grand.Domain.Permissions;
-using Grand.Domain.Discounts;
-using Grand.Web.Common.Controllers;
-using Microsoft.AspNetCore.Mvc;
-using System.Globalization;
-
-namespace DiscountRules.Standard.Areas.Store.Controllers;
-
-public class HadSpentAmountController : BaseStorePluginController
-{
- private readonly IDiscountService _discountService;
- private readonly IPermissionService _permissionService;
-
- public HadSpentAmountController(
- IDiscountService discountService,
- IPermissionService permissionService)
- {
- _discountService = discountService;
- _permissionService = permissionService;
- }
-
- public async Task Configure(string discountId, string discountRequirementId)
- {
- if (!await _permissionService.Authorize(StandardPermission.ManageDiscounts))
- return Content("Access denied");
-
- var discount = await _discountService.GetDiscountById(discountId);
- if (discount == null)
- throw new ArgumentException("Discount could not be loaded");
-
- double spentAmountRequirement = 0;
- if (!string.IsNullOrEmpty(discountRequirementId))
- {
- var discountRequirement = discount.DiscountRules.FirstOrDefault(dr => dr.Id == discountRequirementId);
- if (discountRequirement == null)
- return Content("Failed to load requirement.");
-
- spentAmountRequirement = Convert.ToDouble(discountRequirement.Metadata);
- }
-
- var model = new RequirementSpentAmountModel {
- RequirementId = !string.IsNullOrEmpty(discountRequirementId) ? discountRequirementId : "",
- DiscountId = discountId,
- SpentAmount = spentAmountRequirement
- };
-
- //add a prefix
- ViewData.TemplateInfo.HtmlFieldPrefix =
- $"DiscountRulesHadSpentAmount{discount.Id}-{(!string.IsNullOrEmpty(discountRequirementId) ? discountRequirementId : "")}";
-
- return View(model);
- }
-
- [HttpPost]
- [AutoValidateAntiforgeryToken]
- public async Task Configure(string discountId, string discountRequirementId, double spentAmount)
- {
- if (!await _permissionService.Authorize(StandardPermission.ManageDiscounts))
- return Content("Access denied");
-
- var discount = await _discountService.GetDiscountById(discountId);
- if (discount == null)
- throw new ArgumentException("Discount could not be loaded");
-
- DiscountRule discountRequirement = null;
- if (!string.IsNullOrEmpty(discountRequirementId))
- discountRequirement = discount.DiscountRules.FirstOrDefault(dr => dr.Id == discountRequirementId);
-
- if (discountRequirement != null)
- {
- //update existing rule
- discountRequirement.Metadata = spentAmount.ToString(CultureInfo.InvariantCulture);
- await _discountService.UpdateDiscount(discount);
- }
- else
- {
- //save new rule
- discountRequirement = new DiscountRule {
- DiscountRequirementRuleSystemName = "DiscountRules.Standard.HadSpentAmount",
- Metadata = spentAmount.ToString(CultureInfo.InvariantCulture)
- };
- discount.DiscountRules.Add(discountRequirement);
- await _discountService.UpdateDiscount(discount);
- }
-
- return new JsonResult(new { Result = true, NewRequirementId = discountRequirement.Id });
- }
-}
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/HasAllProductsController.cs b/src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/HasAllProductsController.cs
deleted file mode 100644
index 98e20d7dd..000000000
--- a/src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/HasAllProductsController.cs
+++ /dev/null
@@ -1,227 +0,0 @@
-using DiscountRules.Standard.Models;
-using Grand.Business.Core.Interfaces.Catalog.Discounts;
-using Grand.Business.Core.Interfaces.Catalog.Products;
-using Grand.Business.Core.Interfaces.Common.Localization;
-using Grand.Business.Core.Interfaces.Common.Security;
-using Grand.Business.Core.Interfaces.Common.Stores;
-using Grand.Business.Core.Interfaces.Customers;
-using Grand.Domain.Catalog;
-using Grand.Domain.Discounts;
-using Grand.Domain.Permissions;
-using Grand.Infrastructure;
-using Grand.Web.Common.Controllers;
-using Grand.Web.Common.DataSource;
-using Grand.Web.Common.Localization;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.Rendering;
-
-namespace DiscountRules.Standard.Areas.Store.Controllers;
-
-public class HasAllProductsController : BaseStorePluginController
-{
- private readonly IDiscountService _discountService;
- private readonly IPermissionService _permissionService;
- private readonly IProductService _productService;
- private readonly IStoreService _storeService;
- private readonly ITranslationService _translationService;
- private readonly IVendorService _vendorService;
- private readonly IContextAccessor _contextAccessor;
- private readonly IEnumTranslationService _enumTranslationService;
-
- private string CurrentStoreId => _contextAccessor.WorkContext.CurrentCustomer.StaffStoreId;
-
- public HasAllProductsController(IDiscountService discountService,
- IPermissionService permissionService,
- IContextAccessor contextAccessor,
- ITranslationService translationService,
- IStoreService storeService,
- IVendorService vendorService,
- IProductService productService,
- IEnumTranslationService enumTranslationService)
- {
- _discountService = discountService;
- _permissionService = permissionService;
- _contextAccessor = contextAccessor;
- _translationService = translationService;
- _storeService = storeService;
- _vendorService = vendorService;
- _productService = productService;
- _enumTranslationService = enumTranslationService;
- }
-
- public async Task Configure(string discountId, string discountRequirementId)
- {
- if (!await AuthorizeManageDiscounts())
- return Content("Access denied");
-
- var discount = await GetDiscountById(discountId);
- if (discount == null)
- throw new ArgumentException("Discount could not be loaded");
-
- var restrictedProductIds = string.Empty;
-
- if (!string.IsNullOrEmpty(discountRequirementId))
- {
- var discountRequirement = discount.DiscountRules.FirstOrDefault(dr => dr.Id == discountRequirementId);
- if (discountRequirement == null)
- return Content("Failed to load requirement.");
-
- restrictedProductIds = discountRequirement.Metadata;
- }
-
- var model = new RequirementAllProductsModel {
- RequirementId = !string.IsNullOrEmpty(discountRequirementId) ? discountRequirementId : "",
- DiscountId = discountId,
- Products = restrictedProductIds
- };
-
- //add a prefix
- ViewData.TemplateInfo.HtmlFieldPrefix =
- $"DiscountRulesHasAllProducts{discount.Id}-{(!string.IsNullOrEmpty(discountRequirementId) ? discountRequirementId : "")}";
-
- return View(model);
- }
-
- [HttpPost]
- [AutoValidateAntiforgeryToken]
- public async Task Configure(string discountId, string discountRequirementId, string productIds)
- {
- if (!await AuthorizeManageDiscounts())
- return Content("Access denied");
-
- var discount = await GetDiscountById(discountId);
- if (discount == null)
- throw new ArgumentException("Discount could not be loaded");
-
- DiscountRule discountRequirement = null;
-
- if (!string.IsNullOrEmpty(discountRequirementId))
- discountRequirement = discount.DiscountRules.FirstOrDefault(dr => dr.Id == discountRequirementId);
-
- if (discountRequirement != null)
- {
- //update existing rule
- discountRequirement.Metadata = productIds;
- await _discountService.UpdateDiscount(discount);
- }
- else
- {
- //save new rule
- discountRequirement = new DiscountRule {
- DiscountRequirementRuleSystemName = "DiscountRules.HasAllProducts",
- Metadata = productIds
- };
- discount.DiscountRules.Add(discountRequirement);
- await _discountService.UpdateDiscount(discount);
- }
-
- return new JsonResult(new { Result = true, NewRequirementId = discountRequirement.Id });
- }
-
- public async Task ProductAddPopup(string btnId, string productIdsInput)
- {
- if (!await AuthorizeManageProducts())
- return Content("Access denied");
-
- var model = new RequirementAllProductsModel.AddProductModel();
-
- //stores - scoped to current store manager's store
- model.AvailableStores.Add(new SelectListItem { Text = _translationService.GetResource("Admin.Common.All"), Value = "" });
- foreach (var s in await _storeService.GetAllStores())
- model.AvailableStores.Add(new SelectListItem { Text = s.Shortcut, Value = s.Id });
-
- //product types
- model.AvailableProductTypes = _enumTranslationService.ToSelectList(ProductType.SimpleProduct, false).ToList();
- model.AvailableProductTypes.Insert(0,
- new SelectListItem { Text = _translationService.GetResource("Admin.Common.All"), Value = "" });
-
- ViewBag.productIdsInput = productIdsInput;
- ViewBag.btnId = btnId;
-
- return View(model);
- }
-
- [HttpPost]
- [AutoValidateAntiforgeryToken]
- public async Task ProductAddPopupList(DataSourceRequest command,
- RequirementAllProductsModel.AddProductModel model)
- {
- if (!await AuthorizeManageProducts())
- return Content("Access denied");
-
- var searchCategoryIds = new List();
- if (!string.IsNullOrEmpty(model.SearchCategoryId))
- searchCategoryIds.Add(model.SearchCategoryId);
-
- var products = (await _productService.SearchProducts(
- categoryIds: searchCategoryIds,
- collectionId: model.SearchCollectionId,
- storeId: !string.IsNullOrEmpty(model.SearchStoreId) ? model.SearchStoreId : CurrentStoreId,
- productType: model.SearchProductTypeId > 0 ? (ProductType?)model.SearchProductTypeId : null,
- keywords: model.SearchProductName,
- pageIndex: command.Page - 1,
- pageSize: command.PageSize,
- showHidden: true
- )).products;
- var gridModel = new DataSourceResult {
- Data = products.Select(x => new RequirementAllProductsModel.ProductModel {
- Id = x.Id,
- Name = x.Name,
- Published = x.Published
- }),
- Total = products.TotalCount
- };
-
- return new JsonResult(gridModel);
- }
-
- [HttpPost]
- [AutoValidateAntiforgeryToken]
- public async Task LoadProductFriendlyNames(string productIds)
- {
- var result = "";
-
- if (!await AuthorizeManageProducts())
- return new JsonResult(new { Text = result });
-
- if (string.IsNullOrWhiteSpace(productIds)) return new JsonResult(new { Text = result });
- var ids = new List();
- var rangeArray = productIds
- .Split([','], StringSplitOptions.RemoveEmptyEntries)
- .Select(x => x.Trim())
- .ToList();
-
- foreach (var str1 in rangeArray)
- {
- var str2 = str1;
- if (str2.Contains(':'))
- str2 = str2[..str2.IndexOf(":", StringComparison.Ordinal)];
- ids.Add(str2);
- }
-
- var products = await _productService.GetProductsByIds(ids.ToArray(), true);
- for (var i = 0; i <= products.Count - 1; i++)
- {
- result += products[i].Name;
- if (i != products.Count - 1)
- result += ", ";
- }
-
- return new JsonResult(new { Text = result });
- }
-
- private async Task AuthorizeManageDiscounts()
- {
- return await _permissionService.Authorize(StandardPermission.ManageDiscounts);
- }
-
- private async Task AuthorizeManageProducts()
- {
- return await _permissionService.Authorize(StandardPermission.ManageProducts);
- }
-
- private async Task GetDiscountById(string discountId)
- {
- return await _discountService.GetDiscountById(discountId);
- }
-}
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/HasOneProductController.cs b/src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/HasOneProductController.cs
deleted file mode 100644
index b707f3a2f..000000000
--- a/src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/HasOneProductController.cs
+++ /dev/null
@@ -1,207 +0,0 @@
-using DiscountRules.Standard.Models;
-using Grand.Business.Core.Interfaces.Catalog.Discounts;
-using Grand.Business.Core.Interfaces.Catalog.Products;
-using Grand.Business.Core.Interfaces.Common.Localization;
-using Grand.Business.Core.Interfaces.Common.Security;
-using Grand.Business.Core.Interfaces.Common.Stores;
-using Grand.Business.Core.Interfaces.Customers;
-using Grand.Domain.Catalog;
-using Grand.Domain.Discounts;
-using Grand.Domain.Permissions;
-using Grand.Infrastructure;
-using Grand.Web.Common.Controllers;
-using Grand.Web.Common.DataSource;
-using Grand.Web.Common.Localization;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.Rendering;
-
-namespace DiscountRules.Standard.Areas.Store.Controllers;
-
-public class HasOneProductController : BaseStorePluginController
-{
- private readonly IDiscountService _discountService;
- private readonly IPermissionService _permissionService;
- private readonly IProductService _productService;
- private readonly IStoreService _storeService;
- private readonly ITranslationService _translationService;
- private readonly IVendorService _vendorService;
- private readonly IContextAccessor _contextAccessor;
- private readonly IEnumTranslationService _enumTranslationService;
-
- private string CurrentStoreId => _contextAccessor.WorkContext.CurrentCustomer.StaffStoreId;
-
- public HasOneProductController(IDiscountService discountService,
- IPermissionService permissionService,
- IContextAccessor contextAccessor,
- ITranslationService translationService,
- IStoreService storeService,
- IVendorService vendorService,
- IProductService productService,
- IEnumTranslationService enumTranslationService)
- {
- _discountService = discountService;
- _permissionService = permissionService;
- _contextAccessor = contextAccessor;
- _translationService = translationService;
- _storeService = storeService;
- _vendorService = vendorService;
- _productService = productService;
- _enumTranslationService = enumTranslationService;
- }
-
- private async Task AuthorizeAsync(Permission permission)
- {
- if (!await _permissionService.Authorize(permission))
- return Content("Access denied");
- return null;
- }
-
- private async Task GetDiscountAsync(string discountId)
- {
- var discount = await _discountService.GetDiscountById(discountId);
- if (discount == null)
- throw new ArgumentException("Discount could not be loaded");
- return discount;
- }
-
- public async Task Configure(string discountId, string discountRequirementId)
- {
- var authResult = await AuthorizeAsync(StandardPermission.ManageDiscounts);
- if (authResult != null) return authResult;
-
- var discount = await GetDiscountAsync(discountId);
-
- var restrictedProductIds = string.Empty;
- if (!string.IsNullOrEmpty(discountRequirementId))
- {
- var discountRequirement = discount.DiscountRules.FirstOrDefault(dr => dr.Id == discountRequirementId);
- if (discountRequirement == null)
- return Content("Failed to load requirement.");
-
- restrictedProductIds = discountRequirement.Metadata;
- }
-
- var model = new RequirementOneProductModel {
- RequirementId = !string.IsNullOrEmpty(discountRequirementId) ? discountRequirementId : "",
- DiscountId = discountId,
- Products = restrictedProductIds
- };
-
- //add a prefix
- ViewData.TemplateInfo.HtmlFieldPrefix =
- $"DiscountRulesHasOneProduct{discount.Id}-{(!string.IsNullOrEmpty(discountRequirementId) ? discountRequirementId : "")}";
-
- return View(model);
- }
-
- [HttpPost]
- [AutoValidateAntiforgeryToken]
- public async Task Configure(string discountId, string discountRequirementId, string productIds)
- {
- var authResult = await AuthorizeAsync(StandardPermission.ManageDiscounts);
- if (authResult != null) return authResult;
-
- var discount = await GetDiscountAsync(discountId);
-
- DiscountRule discountRequirement = null;
- if (!string.IsNullOrEmpty(discountRequirementId))
- discountRequirement = discount.DiscountRules.FirstOrDefault(dr => dr.Id == discountRequirementId);
-
- if (discountRequirement != null)
- {
- //update existing rule
- discountRequirement.Metadata = productIds;
- await _discountService.UpdateDiscount(discount);
- }
- else
- {
- //save new rule
- discountRequirement = new DiscountRule {
- DiscountRequirementRuleSystemName = "DiscountRules.HasOneProduct",
- Metadata = productIds
- };
- discount.DiscountRules.Add(discountRequirement);
- await _discountService.UpdateDiscount(discount);
- }
-
- return new JsonResult(new { Result = true, NewRequirementId = discountRequirement.Id });
- }
-
- public async Task ProductAddPopup(string btnId, string productIdsInput)
- {
- var authResult = await AuthorizeAsync(StandardPermission.ManageProducts);
- if (authResult != null) return authResult;
-
- var model = new RequirementOneProductModel.AddProductModel();
-
- //stores - scoped to current store manager's store
- model.AvailableStores.Add(new SelectListItem { Text = _translationService.GetResource("Admin.Common.All"), Value = "" });
- foreach (var s in await _storeService.GetAllStores())
- model.AvailableStores.Add(new SelectListItem { Text = s.Shortcut, Value = s.Id });
-
- //product types
- model.AvailableProductTypes = _enumTranslationService.ToSelectList(ProductType.SimpleProduct, false).ToList();
- model.AvailableProductTypes.Insert(0,
- new SelectListItem { Text = _translationService.GetResource("Admin.Common.All"), Value = "" });
-
- ViewBag.productIdsInput = productIdsInput;
- ViewBag.btnId = btnId;
-
- return View(model);
- }
-
- [HttpPost]
- [AutoValidateAntiforgeryToken]
- public async Task ProductAddPopupList(DataSourceRequest command,
- RequirementOneProductModel.AddProductModel model)
- {
- var authResult = await AuthorizeAsync(StandardPermission.ManageProducts);
- if (authResult != null) return authResult;
-
- var searchCategoryIds = new List();
- if (!string.IsNullOrEmpty(model.SearchCategoryId))
- searchCategoryIds.Add(model.SearchCategoryId);
-
- var products = (await _productService.SearchProducts(
- categoryIds: searchCategoryIds,
- collectionId: model.SearchCollectionId,
- storeId: !string.IsNullOrEmpty(model.SearchStoreId) ? model.SearchStoreId : CurrentStoreId,
- productType: model.SearchProductTypeId > 0 ? (ProductType?)model.SearchProductTypeId : null,
- keywords: model.SearchProductName,
- pageIndex: command.Page - 1,
- pageSize: command.PageSize,
- showHidden: true
- )).products;
- var gridModel = new DataSourceResult {
- Data = products.Select(x => new RequirementOneProductModel.ProductModel {
- Id = x.Id,
- Name = x.Name,
- Published = x.Published
- }),
- Total = products.TotalCount
- };
-
- return new JsonResult(gridModel);
- }
-
- [HttpPost]
- [AutoValidateAntiforgeryToken]
- public async Task LoadProductFriendlyNames(string productIds)
- {
- var authResult = await AuthorizeAsync(StandardPermission.ManageProducts);
- if (authResult != null) return authResult;
-
- var result = "";
-
- if (string.IsNullOrWhiteSpace(productIds)) return new JsonResult(new { Text = result });
- var ids = productIds
- .Split(',', StringSplitOptions.RemoveEmptyEntries)
- .Select(x => x.Split(':')[0].Trim())
- .ToList();
-
- var products = await _productService.GetProductsByIds(ids.ToArray(), true);
- result = string.Join(", ", products.Select(p => p.Name));
-
- return new JsonResult(new { Text = result });
- }
-}
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/ShoppingCartAmountController.cs b/src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/ShoppingCartAmountController.cs
deleted file mode 100644
index 9875942d8..000000000
--- a/src/Plugins/DiscountRules.Standard/Areas/Store/Controllers/ShoppingCartAmountController.cs
+++ /dev/null
@@ -1,89 +0,0 @@
-using DiscountRules.Standard.Models;
-using Grand.Business.Core.Interfaces.Catalog.Discounts;
-using Grand.Business.Core.Interfaces.Common.Security;
-using Grand.Domain.Permissions;
-using Grand.Domain.Discounts;
-using Grand.Web.Common.Controllers;
-using Microsoft.AspNetCore.Mvc;
-using System.Globalization;
-
-namespace DiscountRules.Standard.Areas.Store.Controllers;
-
-public class ShoppingCartAmountController : BaseStorePluginController
-{
- private readonly IDiscountService _discountService;
- private readonly IPermissionService _permissionService;
-
- public ShoppingCartAmountController(IDiscountService discountService,
- IPermissionService permissionService)
- {
- _discountService = discountService;
- _permissionService = permissionService;
- }
-
- public async Task Configure(string discountId, string discountRequirementId)
- {
- if (!await _permissionService.Authorize(StandardPermission.ManageDiscounts))
- return Content("Access denied");
-
- var discount = await _discountService.GetDiscountById(discountId);
- if (discount == null)
- throw new ArgumentException("Discount could not be loaded");
-
- double spentAmountRequirement = 0;
- if (!string.IsNullOrEmpty(discountRequirementId))
- {
- var discountRequirement = discount.DiscountRules.FirstOrDefault(dr => dr.Id == discountRequirementId);
- if (discountRequirement == null)
- return Content("Failed to load requirement.");
-
- spentAmountRequirement = Convert.ToDouble(discountRequirement.Metadata);
- }
-
- var model = new RequirementShoppingCartModel {
- RequirementId = !string.IsNullOrEmpty(discountRequirementId) ? discountRequirementId : "",
- DiscountId = discountId,
- SpentAmount = spentAmountRequirement
- };
-
- //add a prefix
- ViewData.TemplateInfo.HtmlFieldPrefix =
- $"DiscountRulesShoppingCart{(!string.IsNullOrEmpty(discountRequirementId) ? discountRequirementId : "")}";
-
- return View(model);
- }
-
- [HttpPost]
- public async Task Configure(string discountId, string discountRequirementId, double spentAmount)
- {
- if (!await _permissionService.Authorize(StandardPermission.ManageDiscounts))
- return Content("Access denied");
-
- var discount = await _discountService.GetDiscountById(discountId);
- if (discount == null)
- throw new ArgumentException("Discount could not be loaded");
-
- DiscountRule discountRequirement = null;
- if (!string.IsNullOrEmpty(discountRequirementId))
- discountRequirement = discount.DiscountRules.FirstOrDefault(dr => dr.Id == discountRequirementId);
-
- if (discountRequirement != null)
- {
- //update existing rule
- discountRequirement.Metadata = spentAmount.ToString(CultureInfo.InvariantCulture);
- await _discountService.UpdateDiscount(discount);
- }
- else
- {
- //save new rule
- discountRequirement = new DiscountRule {
- DiscountRequirementRuleSystemName = "DiscountRequirement.ShoppingCart",
- Metadata = spentAmount.ToString(CultureInfo.InvariantCulture)
- };
- discount.DiscountRules.Add(discountRequirement);
- await _discountService.UpdateDiscount(discount);
- }
-
- return Json(new { Result = true, NewRequirementId = discountRequirement.Id });
- }
-}
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Store/Views/CustomerGroups/Configure.cshtml b/src/Plugins/DiscountRules.Standard/Areas/Store/Views/CustomerGroups/Configure.cshtml
deleted file mode 100644
index 3e2439120..000000000
--- a/src/Plugins/DiscountRules.Standard/Areas/Store/Views/CustomerGroups/Configure.cshtml
+++ /dev/null
@@ -1,54 +0,0 @@
-@{
- Layout = "";
-}
-@using System.Text.Encodings.Web
-@model DiscountRules.Standard.Models.RequirementCustomerGroupsModel
-
-
-
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Store/Views/HadSpentAmount/Configure.cshtml b/src/Plugins/DiscountRules.Standard/Areas/Store/Views/HadSpentAmount/Configure.cshtml
deleted file mode 100644
index 747f4c264..000000000
--- a/src/Plugins/DiscountRules.Standard/Areas/Store/Views/HadSpentAmount/Configure.cshtml
+++ /dev/null
@@ -1,53 +0,0 @@
-@{
- Layout = "";
-}
-@using System.Text.Encodings.Web
-@model DiscountRules.Standard.Models.RequirementSpentAmountModel
-
-
-
-
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Store/Views/HasAllProducts/Configure.cshtml b/src/Plugins/DiscountRules.Standard/Areas/Store/Views/HasAllProducts/Configure.cshtml
deleted file mode 100644
index ba45cd06b..000000000
--- a/src/Plugins/DiscountRules.Standard/Areas/Store/Views/HasAllProducts/Configure.cshtml
+++ /dev/null
@@ -1,124 +0,0 @@
-@{
- Layout = "";
-}
-@using System.Text.Encodings.Web
-@model DiscountRules.Standard.Models.RequirementAllProductsModel
-
-
-
-
-
-
-
-
-
-
@Loc["admin.marketing.Discounts.Requirements.Saved"]
-
-
@Loc["Common.Wait..."]
-
-
-
-
-
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Store/Views/HasAllProducts/ProductAddPopup.cshtml b/src/Plugins/DiscountRules.Standard/Areas/Store/Views/HasAllProducts/ProductAddPopup.cshtml
deleted file mode 100644
index b4df0f4e2..000000000
--- a/src/Plugins/DiscountRules.Standard/Areas/Store/Views/HasAllProducts/ProductAddPopup.cshtml
+++ /dev/null
@@ -1,193 +0,0 @@
-@{
- Layout = "";
-}
-@using Grand.Domain.Common
-@model DiscountRules.Standard.Models.RequirementAllProductsModel.AddProductModel
-@inject AdminAreaSettings adminAreaSettings
-@{
- //page title
- ViewBag.Title = Loc["Plugins.DiscountRules.HasAllProducts.Fields.Products.Choose"];
-}
-
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Store/Views/HasOneProduct/Configure.cshtml b/src/Plugins/DiscountRules.Standard/Areas/Store/Views/HasOneProduct/Configure.cshtml
deleted file mode 100644
index b98eb9e8e..000000000
--- a/src/Plugins/DiscountRules.Standard/Areas/Store/Views/HasOneProduct/Configure.cshtml
+++ /dev/null
@@ -1,125 +0,0 @@
-@{
- Layout = "";
-}
-@using System.Text.Encodings.Web
-@model DiscountRules.Standard.Models.RequirementOneProductModel
-
-
-
-
-
-
-
-
-
-
-
@Loc["admin.marketing.Discounts.Requirements.Saved"]
-
@Loc["Common.Wait..."]
-
-
-
-
-
-
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Store/Views/HasOneProduct/ProductAddPopup.cshtml b/src/Plugins/DiscountRules.Standard/Areas/Store/Views/HasOneProduct/ProductAddPopup.cshtml
deleted file mode 100644
index 1ff88101d..000000000
--- a/src/Plugins/DiscountRules.Standard/Areas/Store/Views/HasOneProduct/ProductAddPopup.cshtml
+++ /dev/null
@@ -1,192 +0,0 @@
-@{
- Layout = "";
-}
-@using Grand.Domain.Common
-@model DiscountRules.Standard.Models.RequirementOneProductModel.AddProductModel
-@inject AdminAreaSettings adminAreaSettings
-@{
- //page title
- ViewBag.Title = Loc["Plugins.DiscountRules.HasOneProduct.Fields.Products.Choose"];
-}
-
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Store/Views/ShoppingCartAmount/Configure.cshtml b/src/Plugins/DiscountRules.Standard/Areas/Store/Views/ShoppingCartAmount/Configure.cshtml
deleted file mode 100644
index fe3d8f5dc..000000000
--- a/src/Plugins/DiscountRules.Standard/Areas/Store/Views/ShoppingCartAmount/Configure.cshtml
+++ /dev/null
@@ -1,45 +0,0 @@
-@{
- Layout = "";
-}
-@using System.Text.Encodings.Web
-@model DiscountRules.Standard.Models.RequirementShoppingCartModel
-
-
-
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Store/Views/_ViewImports.cshtml b/src/Plugins/DiscountRules.Standard/Areas/Store/Views/_ViewImports.cshtml
deleted file mode 100644
index e02b25523..000000000
--- a/src/Plugins/DiscountRules.Standard/Areas/Store/Views/_ViewImports.cshtml
+++ /dev/null
@@ -1,11 +0,0 @@
-@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
-@addTagHelper *, Grand.Web.Common
-@addTagHelper *, Grand.Web
-
-@using Microsoft.AspNetCore.Mvc.ViewFeatures
-@using Grand.Infrastructure.Extensions
-@using Grand.Web.Common
-@using Grand.Web.Common.Localization
-@using Grand.Web.Common.Page
-
-@inject LocService Loc
diff --git a/src/Plugins/DiscountRules.Standard/Controllers/BaseDiscountRulePluginController.cs b/src/Plugins/DiscountRules.Standard/Controllers/BaseDiscountRulePluginController.cs
new file mode 100644
index 000000000..746cb2d2e
--- /dev/null
+++ b/src/Plugins/DiscountRules.Standard/Controllers/BaseDiscountRulePluginController.cs
@@ -0,0 +1,16 @@
+using Grand.Web.Common.Controllers;
+using Grand.Web.Common.Filters;
+using Microsoft.AspNetCore.Mvc;
+
+namespace DiscountRules.Standard.Controllers;
+
+///
+/// Base controller for discount rule plugins.
+/// Bypasses the Admin-only gate so that store managers can also access the
+/// requirement-configuration actions (which live at both Admin/… and Store/…
+/// routes). Authorization is enforced per-action via IPermissionService.
+///
+[AuthorizeAdmin(ignore: true)]
+[Area("Admin")]
+[AuthorizeMenu]
+public abstract class BaseDiscountRulePluginController : BaseController;
diff --git a/src/Plugins/DiscountRules.Standard/Providers/CustomerGroupDiscountRule.cs b/src/Plugins/DiscountRules.Standard/Providers/CustomerGroupDiscountRule.cs
index ed6a6293c..8687331b3 100644
--- a/src/Plugins/DiscountRules.Standard/Providers/CustomerGroupDiscountRule.cs
+++ b/src/Plugins/DiscountRules.Standard/Providers/CustomerGroupDiscountRule.cs
@@ -38,10 +38,10 @@ public async Task CheckRequirement(DiscountRuleVal
/// Discount identifier
/// Discount requirement identifier (if editing)
/// URL
- public string GetConfigurationUrl(string discountId, string discountRequirementId)
+ public string GetConfigurationUrl(string discountId, string discountRequirementId, string area = "Admin")
{
//configured
- var result = "Admin/CustomerGroups/Configure/?discountId=" + discountId;
+ var result = $"{area}/CustomerGroups/Configure/?discountId=" + discountId;
if (!string.IsNullOrEmpty(discountRequirementId))
result += $"&discountRequirementId={discountRequirementId}";
return result;
diff --git a/src/Plugins/DiscountRules.Standard/Providers/HadSpentAmountDiscountRule.cs b/src/Plugins/DiscountRules.Standard/Providers/HadSpentAmountDiscountRule.cs
index 0e70c5ba8..669b4aa4c 100644
--- a/src/Plugins/DiscountRules.Standard/Providers/HadSpentAmountDiscountRule.cs
+++ b/src/Plugins/DiscountRules.Standard/Providers/HadSpentAmountDiscountRule.cs
@@ -62,10 +62,10 @@ public async Task CheckRequirement(DiscountRuleVal
/// Discount identifier
/// Discount requirement identifier (if editing)
/// URL
- public string GetConfigurationUrl(string discountId, string discountRequirementId)
+ public string GetConfigurationUrl(string discountId, string discountRequirementId, string area = "Admin")
{
//configured
- var result = "Admin/HadSpentAmount/Configure/?discountId=" + discountId;
+ var result = $"{area}/HadSpentAmount/Configure/?discountId=" + discountId;
if (!string.IsNullOrEmpty(discountRequirementId))
result += $"&discountRequirementId={discountRequirementId}";
return result;
diff --git a/src/Plugins/DiscountRules.Standard/Providers/HasAllProductsDiscountRule.cs b/src/Plugins/DiscountRules.Standard/Providers/HasAllProductsDiscountRule.cs
index d8942dadb..5ae2d0a94 100644
--- a/src/Plugins/DiscountRules.Standard/Providers/HasAllProductsDiscountRule.cs
+++ b/src/Plugins/DiscountRules.Standard/Providers/HasAllProductsDiscountRule.cs
@@ -121,10 +121,10 @@ into g
/// Discount identifier
/// Discount requirement identifier (if editing)
/// URL
- public string GetConfigurationUrl(string discountId, string discountRequirementId)
+ public string GetConfigurationUrl(string discountId, string discountRequirementId, string area = "Admin")
{
//configured
- var result = "Admin/HasAllProducts/Configure/?discountId=" + discountId;
+ var result = $"{area}/HasAllProducts/Configure/?discountId=" + discountId;
if (!string.IsNullOrEmpty(discountRequirementId))
result += $"&discountRequirementId={discountRequirementId}";
return result;
diff --git a/src/Plugins/DiscountRules.Standard/Providers/HasOneProductDiscountRule.cs b/src/Plugins/DiscountRules.Standard/Providers/HasOneProductDiscountRule.cs
index 0ed59bd6f..321ad12d7 100644
--- a/src/Plugins/DiscountRules.Standard/Providers/HasOneProductDiscountRule.cs
+++ b/src/Plugins/DiscountRules.Standard/Providers/HasOneProductDiscountRule.cs
@@ -120,10 +120,10 @@ into g
/// Discount identifier
/// Discount requirement identifier (if editing)
/// URL
- public string GetConfigurationUrl(string discountId, string discountRequirementId)
+ public string GetConfigurationUrl(string discountId, string discountRequirementId, string area = "Admin")
{
//configured
- var result = "Admin/HasOneProduct/Configure/?discountId=" + discountId;
+ var result = $"{area}/HasOneProduct/Configure/?discountId=" + discountId;
if (!string.IsNullOrEmpty(discountRequirementId))
result += $"&discountRequirementId={discountRequirementId}";
return result;
diff --git a/src/Plugins/DiscountRules.Standard/Providers/ShoppingCartDiscountRule.cs b/src/Plugins/DiscountRules.Standard/Providers/ShoppingCartDiscountRule.cs
index b5c6c166b..43e08547a 100644
--- a/src/Plugins/DiscountRules.Standard/Providers/ShoppingCartDiscountRule.cs
+++ b/src/Plugins/DiscountRules.Standard/Providers/ShoppingCartDiscountRule.cs
@@ -83,10 +83,10 @@ public async Task CheckRequirement(DiscountRuleVal
/// Discount identifier
/// Discount requirement identifier (if editing)
/// URL
- public string GetConfigurationUrl(string discountId, string discountRequirementId)
+ public string GetConfigurationUrl(string discountId, string discountRequirementId, string area = "Admin")
{
//configured
- var result = "Admin/ShoppingCartAmount/Configure/?discountId=" + discountId;
+ var result = $"{area}/ShoppingCartAmount/Configure/?discountId=" + discountId;
if (!string.IsNullOrEmpty(discountRequirementId))
result += $"&discountRequirementId={discountRequirementId}";
return result;
diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/Discounts/DiscountProviderTest.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/Discounts/DiscountProviderTest.cs
index 16520d1b2..ac1a74d6c 100644
--- a/src/Tests/Grand.Business.Catalog.Tests/Services/Discounts/DiscountProviderTest.cs
+++ b/src/Tests/Grand.Business.Catalog.Tests/Services/Discounts/DiscountProviderTest.cs
@@ -37,7 +37,7 @@ public async Task CheckRequirement(DiscountRuleVal
return await Task.FromResult(result);
}
- public string GetConfigurationUrl(string discountId, string discountRequirementId)
+ public string GetConfigurationUrl(string discountId, string discountRequirementId, string area = "Admin")
{
throw new NotImplementedException();
}
diff --git a/src/Web/Grand.Web.Common/Controllers/BaseStorePluginController.cs b/src/Web/Grand.Web.Common/Controllers/BaseStorePluginController.cs
deleted file mode 100644
index 358098058..000000000
--- a/src/Web/Grand.Web.Common/Controllers/BaseStorePluginController.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using Grand.Web.Common.Filters;
-using Microsoft.AspNetCore.Mvc;
-
-namespace Grand.Web.Common.Controllers;
-
-///
-/// Base controller for plugins in the Store area
-///
-[AuthorizeStore]
-[Area("Store")]
-[AuthorizeMenu]
-public abstract class BaseStorePluginController : BaseController;
diff --git a/src/Web/Grand.Web.Store/Controllers/DiscountController.cs b/src/Web/Grand.Web.Store/Controllers/DiscountController.cs
index 58bb2fca9..b8fe95536 100644
--- a/src/Web/Grand.Web.Store/Controllers/DiscountController.cs
+++ b/src/Web/Grand.Web.Store/Controllers/DiscountController.cs
@@ -64,9 +64,7 @@ public DiscountController(
private string BuildStoreRequirementUrl(IDiscountRule discountRequirementRule, Discount discount, string discountRequirementId)
{
var storeLocation = _contextAccessor.StoreContext.CurrentHost.Url.TrimEnd('/');
- var configUrl = discountRequirementRule.GetConfigurationUrl(discount.Id, discountRequirementId);
- if (configUrl.StartsWith("Admin/", StringComparison.OrdinalIgnoreCase))
- configUrl = "Store/" + configUrl[6..];
+ var configUrl = discountRequirementRule.GetConfigurationUrl(discount.Id, discountRequirementId, "Store");
return $"{storeLocation}/{configUrl}";
}
From 2c771952b91c661bbafa447faacb59173ecf6747 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 1 Jun 2026 18:36:39 +0000
Subject: [PATCH 06/16] Fix Store discount requirement URLs via conventional
routes in EndpointProvider
Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com>
---
.../Controllers/CustomerGroupsController.cs | 2 -
.../Controllers/HadSpentAmountController.cs | 2 -
.../Controllers/HasAllProductsController.cs | 2 -
.../Controllers/HasOneProductController.cs | 2 -
.../ShoppingCartAmountController.cs | 2 -
.../EndpointProvider.cs | 51 +++++++++++++++++++
6 files changed, 51 insertions(+), 10 deletions(-)
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/CustomerGroupsController.cs b/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/CustomerGroupsController.cs
index 1e1c2b980..171e0acb4 100644
--- a/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/CustomerGroupsController.cs
+++ b/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/CustomerGroupsController.cs
@@ -10,8 +10,6 @@
namespace DiscountRules.Standard.Areas.Admin.Controllers;
-[Route("Admin/CustomerGroups/[action]")]
-[Route("Store/CustomerGroups/[action]")]
public class CustomerGroupsController : BaseDiscountRulePluginController
{
private readonly IDiscountService _discountService;
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/HadSpentAmountController.cs b/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/HadSpentAmountController.cs
index a2926761d..bc861682c 100644
--- a/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/HadSpentAmountController.cs
+++ b/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/HadSpentAmountController.cs
@@ -9,8 +9,6 @@
namespace DiscountRules.Standard.Areas.Admin.Controllers;
-[Route("Admin/HadSpentAmount/[action]")]
-[Route("Store/HadSpentAmount/[action]")]
public class HadSpentAmountController : BaseDiscountRulePluginController
{
private readonly IDiscountService _discountService;
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/HasAllProductsController.cs b/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/HasAllProductsController.cs
index 3241722b3..e675d35c5 100644
--- a/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/HasAllProductsController.cs
+++ b/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/HasAllProductsController.cs
@@ -17,8 +17,6 @@
namespace DiscountRules.Standard.Areas.Admin.Controllers;
-[Route("Admin/HasAllProducts/[action]")]
-[Route("Store/HasAllProducts/[action]")]
public class HasAllProductsController : BaseDiscountRulePluginController
{
private readonly IDiscountService _discountService;
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/HasOneProductController.cs b/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/HasOneProductController.cs
index e8c5127f9..745bc71af 100644
--- a/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/HasOneProductController.cs
+++ b/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/HasOneProductController.cs
@@ -18,8 +18,6 @@
namespace DiscountRules.Standard.Areas.Admin.Controllers;
-[Route("Admin/HasOneProduct/[action]")]
-[Route("Store/HasOneProduct/[action]")]
public class HasOneProductController : BaseDiscountRulePluginController
{
private readonly IDiscountService _discountService;
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/ShoppingCartAmountController.cs b/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/ShoppingCartAmountController.cs
index 18421a45a..86e4d5ed7 100644
--- a/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/ShoppingCartAmountController.cs
+++ b/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/ShoppingCartAmountController.cs
@@ -9,8 +9,6 @@
namespace DiscountRules.Standard.Areas.Admin.Controllers;
-[Route("Admin/ShoppingCartAmount/[action]")]
-[Route("Store/ShoppingCartAmount/[action]")]
public class ShoppingCartAmountController : BaseDiscountRulePluginController
{
private readonly IDiscountService _discountService;
diff --git a/src/Plugins/DiscountRules.Standard/EndpointProvider.cs b/src/Plugins/DiscountRules.Standard/EndpointProvider.cs
index 47cbed67c..4961b15bc 100644
--- a/src/Plugins/DiscountRules.Standard/EndpointProvider.cs
+++ b/src/Plugins/DiscountRules.Standard/EndpointProvider.cs
@@ -13,48 +13,99 @@ public void RegisterEndpoint(IEndpointRouteBuilder endpointRouteBuilder)
"Admin/CustomerGroups/Configure",
new { controller = "CustomerGroups", action = "Configure" }
);
+ endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.CustomerGroups.Store.Configure",
+ "Store/CustomerGroups/Configure",
+ new { controller = "CustomerGroups", action = "Configure" }
+ );
//HadSpentAmount
endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HadSpentAmount.Configure",
"Admin/HadSpentAmount/Configure",
new { controller = "HadSpentAmount", action = "Configure" }
);
+ endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HadSpentAmount.Store.Configure",
+ "Store/HadSpentAmount/Configure",
+ new { controller = "HadSpentAmount", action = "Configure" }
+ );
+
+ //ShoppingCartAmount
+ //ShoppingCartAmount
+ endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.ShoppingCartAmount.Configure",
+ "Admin/ShoppingCartAmount/Configure",
+ new { controller = "ShoppingCartAmount", action = "Configure" }
+ );
+ endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.ShoppingCartAmount.Store.Configure",
+ "Store/ShoppingCartAmount/Configure",
+ new { controller = "ShoppingCartAmount", action = "Configure" }
+ );
//HasAllProducts
endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasAllProducts.Configure",
"Admin/HasAllProducts/Configure",
new { controller = "HasAllProducts", action = "Configure" }
);
+ endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasAllProducts.Store.Configure",
+ "Store/HasAllProducts/Configure",
+ new { controller = "HasAllProducts", action = "Configure" }
+ );
endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasAllProducts.ProductAddPopup",
"Admin/HasAllProducts/ProductAddPopup",
new { controller = "HasAllProducts", action = "ProductAddPopup" }
);
+ endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasAllProducts.Store.ProductAddPopup",
+ "Store/HasAllProducts/ProductAddPopup",
+ new { controller = "HasAllProducts", action = "ProductAddPopup" }
+ );
endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasAllProducts.ProductAddPopupList",
"Admin/HasAllProducts/ProductAddPopupList",
new { controller = "HasAllProducts", action = "ProductAddPopupList" }
);
+ endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasAllProducts.Store.ProductAddPopupList",
+ "Store/HasAllProducts/ProductAddPopupList",
+ new { controller = "HasAllProducts", action = "ProductAddPopupList" }
+ );
endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasAllProducts.LoadProductFriendlyNames",
"Admin/HasAllProducts/LoadProductFriendlyNames",
new { controller = "HasAllProducts", action = "LoadProductFriendlyNames" }
);
+ endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasAllProducts.Store.LoadProductFriendlyNames",
+ "Store/HasAllProducts/LoadProductFriendlyNames",
+ new { controller = "HasAllProducts", action = "LoadProductFriendlyNames" }
+ );
//HasOneProduct
endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasOneProduct.Configure",
"Admin/HasOneProduct/Configure",
new { controller = "HasOneProduct", action = "Configure" }
);
+ endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasOneProduct.Store.Configure",
+ "Store/HasOneProduct/Configure",
+ new { controller = "HasOneProduct", action = "Configure" }
+ );
endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasOneProduct.ProductAddPopup",
"Admin/HasOneProduct/ProductAddPopup",
new { controller = "HasOneProduct", action = "ProductAddPopup" }
);
+ endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasOneProduct.Store.ProductAddPopup",
+ "Store/HasOneProduct/ProductAddPopup",
+ new { controller = "HasOneProduct", action = "ProductAddPopup" }
+ );
endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasOneProduct.ProductAddPopupList",
"Admin/HasOneProduct/ProductAddPopupList",
new { controller = "HasOneProduct", action = "ProductAddPopupList" }
);
+ endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasOneProduct.Store.ProductAddPopupList",
+ "Store/HasOneProduct/ProductAddPopupList",
+ new { controller = "HasOneProduct", action = "ProductAddPopupList" }
+ );
endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasOneProduct.LoadProductFriendlyNames",
"Admin/HasOneProduct/LoadProductFriendlyNames",
new { controller = "HasOneProduct", action = "LoadProductFriendlyNames" }
);
+ endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasOneProduct.Store.LoadProductFriendlyNames",
+ "Store/HasOneProduct/LoadProductFriendlyNames",
+ new { controller = "HasOneProduct", action = "LoadProductFriendlyNames" }
+ );
}
public int Priority => 0;
From fd02980eb51726b8d17b1e351b18e1e7c3a4b244 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 1 Jun 2026 18:56:17 +0000
Subject: [PATCH 07/16] Fix Store discount requirement URL: use Admin-area URLs
via GetRequirementUrlInternal
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Remove BuildStoreRequirementUrl from Store DiscountController; replace both
calls with _discountViewModelService.GetRequirementUrlInternal which produces
/Admin//Configure URLs — these already work for store managers because
BaseDiscountRulePluginController carries [AuthorizeAdmin(ignore: true)]
- Remove Store-specific plugin routes from DiscountRules.Standard EndpointProvider;
they were shadowed by the storeareas area route (priority 10) before the plugin
routes (priority 0) could match, causing 404 for Store/CustomerGroups/Configure
Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com>
---
.../EndpointProvider.cs | 44 -------------------
.../Controllers/DiscountController.cs | 11 +----
2 files changed, 2 insertions(+), 53 deletions(-)
diff --git a/src/Plugins/DiscountRules.Standard/EndpointProvider.cs b/src/Plugins/DiscountRules.Standard/EndpointProvider.cs
index 4961b15bc..215283e06 100644
--- a/src/Plugins/DiscountRules.Standard/EndpointProvider.cs
+++ b/src/Plugins/DiscountRules.Standard/EndpointProvider.cs
@@ -13,20 +13,12 @@ public void RegisterEndpoint(IEndpointRouteBuilder endpointRouteBuilder)
"Admin/CustomerGroups/Configure",
new { controller = "CustomerGroups", action = "Configure" }
);
- endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.CustomerGroups.Store.Configure",
- "Store/CustomerGroups/Configure",
- new { controller = "CustomerGroups", action = "Configure" }
- );
//HadSpentAmount
endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HadSpentAmount.Configure",
"Admin/HadSpentAmount/Configure",
new { controller = "HadSpentAmount", action = "Configure" }
);
- endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HadSpentAmount.Store.Configure",
- "Store/HadSpentAmount/Configure",
- new { controller = "HadSpentAmount", action = "Configure" }
- );
//ShoppingCartAmount
//ShoppingCartAmount
@@ -34,78 +26,42 @@ public void RegisterEndpoint(IEndpointRouteBuilder endpointRouteBuilder)
"Admin/ShoppingCartAmount/Configure",
new { controller = "ShoppingCartAmount", action = "Configure" }
);
- endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.ShoppingCartAmount.Store.Configure",
- "Store/ShoppingCartAmount/Configure",
- new { controller = "ShoppingCartAmount", action = "Configure" }
- );
//HasAllProducts
endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasAllProducts.Configure",
"Admin/HasAllProducts/Configure",
new { controller = "HasAllProducts", action = "Configure" }
);
- endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasAllProducts.Store.Configure",
- "Store/HasAllProducts/Configure",
- new { controller = "HasAllProducts", action = "Configure" }
- );
endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasAllProducts.ProductAddPopup",
"Admin/HasAllProducts/ProductAddPopup",
new { controller = "HasAllProducts", action = "ProductAddPopup" }
);
- endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasAllProducts.Store.ProductAddPopup",
- "Store/HasAllProducts/ProductAddPopup",
- new { controller = "HasAllProducts", action = "ProductAddPopup" }
- );
endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasAllProducts.ProductAddPopupList",
"Admin/HasAllProducts/ProductAddPopupList",
new { controller = "HasAllProducts", action = "ProductAddPopupList" }
);
- endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasAllProducts.Store.ProductAddPopupList",
- "Store/HasAllProducts/ProductAddPopupList",
- new { controller = "HasAllProducts", action = "ProductAddPopupList" }
- );
endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasAllProducts.LoadProductFriendlyNames",
"Admin/HasAllProducts/LoadProductFriendlyNames",
new { controller = "HasAllProducts", action = "LoadProductFriendlyNames" }
);
- endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasAllProducts.Store.LoadProductFriendlyNames",
- "Store/HasAllProducts/LoadProductFriendlyNames",
- new { controller = "HasAllProducts", action = "LoadProductFriendlyNames" }
- );
//HasOneProduct
endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasOneProduct.Configure",
"Admin/HasOneProduct/Configure",
new { controller = "HasOneProduct", action = "Configure" }
);
- endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasOneProduct.Store.Configure",
- "Store/HasOneProduct/Configure",
- new { controller = "HasOneProduct", action = "Configure" }
- );
endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasOneProduct.ProductAddPopup",
"Admin/HasOneProduct/ProductAddPopup",
new { controller = "HasOneProduct", action = "ProductAddPopup" }
);
- endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasOneProduct.Store.ProductAddPopup",
- "Store/HasOneProduct/ProductAddPopup",
- new { controller = "HasOneProduct", action = "ProductAddPopup" }
- );
endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasOneProduct.ProductAddPopupList",
"Admin/HasOneProduct/ProductAddPopupList",
new { controller = "HasOneProduct", action = "ProductAddPopupList" }
);
- endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasOneProduct.Store.ProductAddPopupList",
- "Store/HasOneProduct/ProductAddPopupList",
- new { controller = "HasOneProduct", action = "ProductAddPopupList" }
- );
endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasOneProduct.LoadProductFriendlyNames",
"Admin/HasOneProduct/LoadProductFriendlyNames",
new { controller = "HasOneProduct", action = "LoadProductFriendlyNames" }
);
- endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasOneProduct.Store.LoadProductFriendlyNames",
- "Store/HasOneProduct/LoadProductFriendlyNames",
- new { controller = "HasOneProduct", action = "LoadProductFriendlyNames" }
- );
}
public int Priority => 0;
diff --git a/src/Web/Grand.Web.Store/Controllers/DiscountController.cs b/src/Web/Grand.Web.Store/Controllers/DiscountController.cs
index b8fe95536..1841fe5a4 100644
--- a/src/Web/Grand.Web.Store/Controllers/DiscountController.cs
+++ b/src/Web/Grand.Web.Store/Controllers/DiscountController.cs
@@ -61,13 +61,6 @@ public DiscountController(
private string CurrentStoreId =>
_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId;
- private string BuildStoreRequirementUrl(IDiscountRule discountRequirementRule, Discount discount, string discountRequirementId)
- {
- var storeLocation = _contextAccessor.StoreContext.CurrentHost.Url.TrimEnd('/');
- var configUrl = discountRequirementRule.GetConfigurationUrl(discount.Id, discountRequirementId, "Store");
- return $"{storeLocation}/{configUrl}";
- }
-
#endregion
#region Discounts
@@ -305,7 +298,7 @@ public async Task GetDiscountRequirementConfigurationUrl(string r
var singleRequirement = discountPlugin.GetRequirementRules().FirstOrDefault(x =>
x.SystemName.Equals(rulesystemName, StringComparison.OrdinalIgnoreCase));
- var url = BuildStoreRequirementUrl(singleRequirement, discount, discountRequirementId);
+ var url = _discountViewModelService.GetRequirementUrlInternal(singleRequirement, discount, discountRequirementId);
return Json(new { url });
}
@@ -332,7 +325,7 @@ public async Task GetDiscountRequirementMetaInfo(string discountR
var discountRequirementRule = discountPlugin.GetRequirementRules()
.First(x => x.SystemName == discountRequirement.DiscountRequirementRuleSystemName);
- var url = BuildStoreRequirementUrl(discountRequirementRule, discount, discountRequirementId);
+ var url = _discountViewModelService.GetRequirementUrlInternal(discountRequirementRule, discount, discountRequirementId);
var ruleName = discountRequirementRule.FriendlyName;
return Json(new { url, ruleName });
From 498eb788fb0b41c01a6c5717c65f5d070f3ee1e9 Mon Sep 17 00:00:00 2001
From: KrzysztofPajak
Date: Wed, 3 Jun 2026 21:42:04 +0200
Subject: [PATCH 08/16] Refactor discount rule plugin for modular architecture
- Removed `area` parameter from `GetConfigurationUrl` method.
- Replaced `Admin` area with custom paths in endpoint routes.
- Refactored and renamed discount rule controllers.
- Updated `BaseDiscountRulePluginController` for store manager access.
- Simplified and standardized views for discount rule configurations.
- Added new views for handling configurations and product selection.
- Updated `_ViewImports.cshtml` for tag helpers and namespaces.
- Introduced new controllers for modular discount rule handling.
- Improved JavaScript for AJAX requests and product-friendly names.
- Added localization support for views using `LocService`.
---
.../Catalog/Discounts/IDiscountRule.cs | 3 +-
.../BaseDiscountRulePluginController.cs | 9 ----
.../DiscountRulesCustomerGroupsController.cs} | 7 ++-
.../DiscountRulesHadSpentAmountController.cs} | 7 ++-
.../DiscountRulesHasAllProductsController.cs} | 7 ++-
.../DiscountRulesHasOneProductController.cs} | 7 ++-
...countRulesShoppingCartAmountController.cs} | 7 ++-
.../DiscountRules.Standard.csproj | 3 ++
.../EndpointProvider.cs | 45 +++++++++----------
.../Providers/CustomerGroupDiscountRule.cs | 4 +-
.../Providers/HadSpentAmountDiscountRule.cs | 4 +-
.../Providers/HasAllProductsDiscountRule.cs | 4 +-
.../Providers/HasOneProductDiscountRule.cs | 4 +-
.../Providers/ShoppingCartDiscountRule.cs | 4 +-
.../Configure.cshtml | 2 +-
.../Configure.cshtml | 2 +-
.../Configure.cshtml | 2 +-
.../ProductAddPopup.cshtml | 0
.../Configure.cshtml | 2 +-
.../ProductAddPopup.cshtml | 2 +-
.../Configure.cshtml | 2 +-
.../Views/_ViewImports.cshtml | 11 +++++
.../Services/DiscountViewModelService.cs | 5 +--
23 files changed, 69 insertions(+), 74 deletions(-)
rename src/Plugins/DiscountRules.Standard/{Areas/Admin/Controllers/CustomerGroupsController.cs => Controllers/DiscountRulesCustomerGroupsController.cs} (94%)
rename src/Plugins/DiscountRules.Standard/{Areas/Admin/Controllers/HadSpentAmountController.cs => Controllers/DiscountRulesHadSpentAmountController.cs} (94%)
rename src/Plugins/DiscountRules.Standard/{Areas/Admin/Controllers/HasAllProductsController.cs => Controllers/DiscountRulesHasAllProductsController.cs} (97%)
rename src/Plugins/DiscountRules.Standard/{Areas/Admin/Controllers/HasOneProductController.cs => Controllers/DiscountRulesHasOneProductController.cs} (97%)
rename src/Plugins/DiscountRules.Standard/{Areas/Admin/Controllers/ShoppingCartAmountController.cs => Controllers/DiscountRulesShoppingCartAmountController.cs} (92%)
rename src/Plugins/DiscountRules.Standard/{Areas/Admin/Views/CustomerGroups => Views/DiscountRulesCustomerGroups}/Configure.cshtml (96%)
rename src/Plugins/DiscountRules.Standard/{Areas/Admin/Views/HadSpentAmount => Views/DiscountRulesHadSpentAmount}/Configure.cshtml (96%)
rename src/Plugins/DiscountRules.Standard/{Areas/Admin/Views/HasAllProducts => Views/DiscountRulesHasAllProducts}/Configure.cshtml (98%)
rename src/Plugins/DiscountRules.Standard/{Areas/Admin/Views/HasAllProducts => Views/DiscountRulesHasAllProducts}/ProductAddPopup.cshtml (100%)
rename src/Plugins/DiscountRules.Standard/{Areas/Admin/Views/HasOneProduct => Views/DiscountRulesHasOneProduct}/Configure.cshtml (98%)
rename src/Plugins/DiscountRules.Standard/{Areas/Admin/Views/HasOneProduct => Views/DiscountRulesHasOneProduct}/ProductAddPopup.cshtml (98%)
rename src/Plugins/DiscountRules.Standard/{Areas/Admin/Views/ShoppingCartAmount => Views/DiscountRulesShoppingCartAmount}/Configure.cshtml (95%)
create mode 100644 src/Plugins/DiscountRules.Standard/Views/_ViewImports.cshtml
diff --git a/src/Business/Grand.Business.Core/Interfaces/Catalog/Discounts/IDiscountRule.cs b/src/Business/Grand.Business.Core/Interfaces/Catalog/Discounts/IDiscountRule.cs
index 30b54022e..512309733 100644
--- a/src/Business/Grand.Business.Core/Interfaces/Catalog/Discounts/IDiscountRule.cs
+++ b/src/Business/Grand.Business.Core/Interfaces/Catalog/Discounts/IDiscountRule.cs
@@ -28,7 +28,6 @@ public interface IDiscountRule
///
/// Discount id
/// Discount requirement id
- /// Area name (e.g. "Admin" or "Store")
/// URL
- string GetConfigurationUrl(string discountId, string discountRequirementId, string area = "Admin");
+ string GetConfigurationUrl(string discountId, string discountRequirementId);
}
\ No newline at end of file
diff --git a/src/Plugins/DiscountRules.Standard/Controllers/BaseDiscountRulePluginController.cs b/src/Plugins/DiscountRules.Standard/Controllers/BaseDiscountRulePluginController.cs
index 746cb2d2e..3444ccb34 100644
--- a/src/Plugins/DiscountRules.Standard/Controllers/BaseDiscountRulePluginController.cs
+++ b/src/Plugins/DiscountRules.Standard/Controllers/BaseDiscountRulePluginController.cs
@@ -1,16 +1,7 @@
using Grand.Web.Common.Controllers;
using Grand.Web.Common.Filters;
-using Microsoft.AspNetCore.Mvc;
namespace DiscountRules.Standard.Controllers;
-///
-/// Base controller for discount rule plugins.
-/// Bypasses the Admin-only gate so that store managers can also access the
-/// requirement-configuration actions (which live at both Admin/… and Store/…
-/// routes). Authorization is enforced per-action via IPermissionService.
-///
-[AuthorizeAdmin(ignore: true)]
-[Area("Admin")]
[AuthorizeMenu]
public abstract class BaseDiscountRulePluginController : BaseController;
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/CustomerGroupsController.cs b/src/Plugins/DiscountRules.Standard/Controllers/DiscountRulesCustomerGroupsController.cs
similarity index 94%
rename from src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/CustomerGroupsController.cs
rename to src/Plugins/DiscountRules.Standard/Controllers/DiscountRulesCustomerGroupsController.cs
index 171e0acb4..f4c836c94 100644
--- a/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/CustomerGroupsController.cs
+++ b/src/Plugins/DiscountRules.Standard/Controllers/DiscountRulesCustomerGroupsController.cs
@@ -1,4 +1,3 @@
-using DiscountRules.Standard.Controllers;
using DiscountRules.Standard.Models;
using Grand.Business.Core.Interfaces.Catalog.Discounts;
using Grand.Business.Core.Interfaces.Common.Directory;
@@ -8,15 +7,15 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
-namespace DiscountRules.Standard.Areas.Admin.Controllers;
+namespace DiscountRules.Standard.Controllers;
-public class CustomerGroupsController : BaseDiscountRulePluginController
+public class DiscountRulesCustomerGroupsController : BaseDiscountRulePluginController
{
private readonly IDiscountService _discountService;
private readonly IGroupService _groupService;
private readonly IPermissionService _permissionService;
- public CustomerGroupsController(
+ public DiscountRulesCustomerGroupsController(
IDiscountService discountService,
IGroupService groupService,
IPermissionService permissionService)
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/HadSpentAmountController.cs b/src/Plugins/DiscountRules.Standard/Controllers/DiscountRulesHadSpentAmountController.cs
similarity index 94%
rename from src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/HadSpentAmountController.cs
rename to src/Plugins/DiscountRules.Standard/Controllers/DiscountRulesHadSpentAmountController.cs
index bc861682c..818f39afc 100644
--- a/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/HadSpentAmountController.cs
+++ b/src/Plugins/DiscountRules.Standard/Controllers/DiscountRulesHadSpentAmountController.cs
@@ -1,4 +1,3 @@
-using DiscountRules.Standard.Controllers;
using DiscountRules.Standard.Models;
using Grand.Business.Core.Interfaces.Catalog.Discounts;
using Grand.Business.Core.Interfaces.Common.Security;
@@ -7,14 +6,14 @@
using Microsoft.AspNetCore.Mvc;
using System.Globalization;
-namespace DiscountRules.Standard.Areas.Admin.Controllers;
+namespace DiscountRules.Standard.Controllers;
-public class HadSpentAmountController : BaseDiscountRulePluginController
+public class DiscountRulesHadSpentAmountController : BaseDiscountRulePluginController
{
private readonly IDiscountService _discountService;
private readonly IPermissionService _permissionService;
- public HadSpentAmountController(
+ public DiscountRulesHadSpentAmountController(
IDiscountService discountService,
IPermissionService permissionService)
{
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/HasAllProductsController.cs b/src/Plugins/DiscountRules.Standard/Controllers/DiscountRulesHasAllProductsController.cs
similarity index 97%
rename from src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/HasAllProductsController.cs
rename to src/Plugins/DiscountRules.Standard/Controllers/DiscountRulesHasAllProductsController.cs
index e675d35c5..16c754fec 100644
--- a/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/HasAllProductsController.cs
+++ b/src/Plugins/DiscountRules.Standard/Controllers/DiscountRulesHasAllProductsController.cs
@@ -1,4 +1,3 @@
-using DiscountRules.Standard.Controllers;
using DiscountRules.Standard.Models;
using Grand.Business.Core.Interfaces.Catalog.Discounts;
using Grand.Business.Core.Interfaces.Catalog.Products;
@@ -15,9 +14,9 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
-namespace DiscountRules.Standard.Areas.Admin.Controllers;
+namespace DiscountRules.Standard.Controllers;
-public class HasAllProductsController : BaseDiscountRulePluginController
+public class DiscountRulesHasAllProductsController : BaseDiscountRulePluginController
{
private readonly IDiscountService _discountService;
private readonly IPermissionService _permissionService;
@@ -29,7 +28,7 @@ public class HasAllProductsController : BaseDiscountRulePluginController
private readonly IEnumTranslationService _enumTranslationService;
private string CurrentStoreId => _contextAccessor.WorkContext.CurrentCustomer.StaffStoreId;
- public HasAllProductsController(IDiscountService discountService,
+ public DiscountRulesHasAllProductsController(IDiscountService discountService,
IPermissionService permissionService,
IContextAccessor contextAccessor,
ITranslationService translationService,
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/HasOneProductController.cs b/src/Plugins/DiscountRules.Standard/Controllers/DiscountRulesHasOneProductController.cs
similarity index 97%
rename from src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/HasOneProductController.cs
rename to src/Plugins/DiscountRules.Standard/Controllers/DiscountRulesHasOneProductController.cs
index 745bc71af..b214e08ad 100644
--- a/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/HasOneProductController.cs
+++ b/src/Plugins/DiscountRules.Standard/Controllers/DiscountRulesHasOneProductController.cs
@@ -1,4 +1,3 @@
-using DiscountRules.Standard.Controllers;
using DiscountRules.Standard.Models;
using Grand.Business.Core.Interfaces.Catalog.Discounts;
using Grand.Business.Core.Interfaces.Catalog.Products;
@@ -16,9 +15,9 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
-namespace DiscountRules.Standard.Areas.Admin.Controllers;
+namespace DiscountRules.Standard.Controllers;
-public class HasOneProductController : BaseDiscountRulePluginController
+public class DiscountRulesHasOneProductController : BaseDiscountRulePluginController
{
private readonly IDiscountService _discountService;
private readonly IPermissionService _permissionService;
@@ -32,7 +31,7 @@ public class HasOneProductController : BaseDiscountRulePluginController
private Vendor CurrentVendor => _contextAccessor.WorkContext.CurrentVendor;
private string CurrentStoreId => _contextAccessor.WorkContext.CurrentCustomer.StaffStoreId;
- public HasOneProductController(IDiscountService discountService,
+ public DiscountRulesHasOneProductController(IDiscountService discountService,
IPermissionService permissionService,
IContextAccessor contextAccessor,
ITranslationService translationService,
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/ShoppingCartAmountController.cs b/src/Plugins/DiscountRules.Standard/Controllers/DiscountRulesShoppingCartAmountController.cs
similarity index 92%
rename from src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/ShoppingCartAmountController.cs
rename to src/Plugins/DiscountRules.Standard/Controllers/DiscountRulesShoppingCartAmountController.cs
index 86e4d5ed7..c047ba4ef 100644
--- a/src/Plugins/DiscountRules.Standard/Areas/Admin/Controllers/ShoppingCartAmountController.cs
+++ b/src/Plugins/DiscountRules.Standard/Controllers/DiscountRulesShoppingCartAmountController.cs
@@ -1,4 +1,3 @@
-using DiscountRules.Standard.Controllers;
using DiscountRules.Standard.Models;
using Grand.Business.Core.Interfaces.Catalog.Discounts;
using Grand.Business.Core.Interfaces.Common.Security;
@@ -7,14 +6,14 @@
using Microsoft.AspNetCore.Mvc;
using System.Globalization;
-namespace DiscountRules.Standard.Areas.Admin.Controllers;
+namespace DiscountRules.Standard.Controllers;
-public class ShoppingCartAmountController : BaseDiscountRulePluginController
+public class DiscountRulesShoppingCartAmountController : BaseDiscountRulePluginController
{
private readonly IDiscountService _discountService;
private readonly IPermissionService _permissionService;
- public ShoppingCartAmountController(IDiscountService discountService,
+ public DiscountRulesShoppingCartAmountController(IDiscountService discountService,
IPermissionService permissionService)
{
_discountService = discountService;
diff --git a/src/Plugins/DiscountRules.Standard/DiscountRules.Standard.csproj b/src/Plugins/DiscountRules.Standard/DiscountRules.Standard.csproj
index c130e63e8..3acf1d982 100644
--- a/src/Plugins/DiscountRules.Standard/DiscountRules.Standard.csproj
+++ b/src/Plugins/DiscountRules.Standard/DiscountRules.Standard.csproj
@@ -44,5 +44,8 @@
Always
+
+
+
\ No newline at end of file
diff --git a/src/Plugins/DiscountRules.Standard/EndpointProvider.cs b/src/Plugins/DiscountRules.Standard/EndpointProvider.cs
index 215283e06..c218ded62 100644
--- a/src/Plugins/DiscountRules.Standard/EndpointProvider.cs
+++ b/src/Plugins/DiscountRules.Standard/EndpointProvider.cs
@@ -10,57 +10,56 @@ public void RegisterEndpoint(IEndpointRouteBuilder endpointRouteBuilder)
{
//CustomerGroups
endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.CustomerGroups.Configure",
- "Admin/CustomerGroups/Configure",
- new { controller = "CustomerGroups", action = "Configure" }
+ "DiscountRulesCustomerGroups/Configure",
+ new { controller = "DiscountRulesCustomerGroups", action = "Configure" }
);
//HadSpentAmount
endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HadSpentAmount.Configure",
- "Admin/HadSpentAmount/Configure",
- new { controller = "HadSpentAmount", action = "Configure" }
+ "DiscountRulesHadSpentAmount/Configure",
+ new { controller = "DiscountRulesHadSpentAmount", action = "Configure" }
);
- //ShoppingCartAmount
//ShoppingCartAmount
endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.ShoppingCartAmount.Configure",
- "Admin/ShoppingCartAmount/Configure",
- new { controller = "ShoppingCartAmount", action = "Configure" }
+ "DiscountRulesShoppingCartAmount/Configure",
+ new { controller = "DiscountRulesShoppingCartAmount", action = "Configure" }
);
//HasAllProducts
endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasAllProducts.Configure",
- "Admin/HasAllProducts/Configure",
- new { controller = "HasAllProducts", action = "Configure" }
+ "DiscountRulesHasAllProducts/Configure",
+ new { controller = "DiscountRulesHasAllProducts", action = "Configure" }
);
endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasAllProducts.ProductAddPopup",
- "Admin/HasAllProducts/ProductAddPopup",
- new { controller = "HasAllProducts", action = "ProductAddPopup" }
+ "DiscountRulesHasAllProducts/ProductAddPopup",
+ new { controller = "DiscountRulesHasAllProducts", action = "ProductAddPopup" }
);
endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasAllProducts.ProductAddPopupList",
- "Admin/HasAllProducts/ProductAddPopupList",
- new { controller = "HasAllProducts", action = "ProductAddPopupList" }
+ "DiscountRulesHasAllProducts/ProductAddPopupList",
+ new { controller = "DiscountRulesHasAllProducts", action = "ProductAddPopupList" }
);
endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasAllProducts.LoadProductFriendlyNames",
- "Admin/HasAllProducts/LoadProductFriendlyNames",
- new { controller = "HasAllProducts", action = "LoadProductFriendlyNames" }
+ "DiscountRulesHasAllProducts/LoadProductFriendlyNames",
+ new { controller = "DiscountRulesHasAllProducts", action = "LoadProductFriendlyNames" }
);
//HasOneProduct
endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasOneProduct.Configure",
- "Admin/HasOneProduct/Configure",
- new { controller = "HasOneProduct", action = "Configure" }
+ "DiscountRulesHasOneProduct/Configure",
+ new { controller = "DiscountRulesHasOneProduct", action = "Configure" }
);
endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasOneProduct.ProductAddPopup",
- "Admin/HasOneProduct/ProductAddPopup",
- new { controller = "HasOneProduct", action = "ProductAddPopup" }
+ "DiscountRulesHasOneProduct/ProductAddPopup",
+ new { controller = "DiscountRulesHasOneProduct", action = "ProductAddPopup" }
);
endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasOneProduct.ProductAddPopupList",
- "Admin/HasOneProduct/ProductAddPopupList",
- new { controller = "HasOneProduct", action = "ProductAddPopupList" }
+ "DiscountRulesHasOneProduct/ProductAddPopupList",
+ new { controller = "DiscountRulesHasOneProduct", action = "ProductAddPopupList" }
);
endpointRouteBuilder.MapControllerRoute("Plugin.DiscountRules.HasOneProduct.LoadProductFriendlyNames",
- "Admin/HasOneProduct/LoadProductFriendlyNames",
- new { controller = "HasOneProduct", action = "LoadProductFriendlyNames" }
+ "DiscountRulesHasOneProduct/LoadProductFriendlyNames",
+ new { controller = "DiscountRulesHasOneProduct", action = "LoadProductFriendlyNames" }
);
}
diff --git a/src/Plugins/DiscountRules.Standard/Providers/CustomerGroupDiscountRule.cs b/src/Plugins/DiscountRules.Standard/Providers/CustomerGroupDiscountRule.cs
index 8687331b3..d0a62c90c 100644
--- a/src/Plugins/DiscountRules.Standard/Providers/CustomerGroupDiscountRule.cs
+++ b/src/Plugins/DiscountRules.Standard/Providers/CustomerGroupDiscountRule.cs
@@ -38,10 +38,10 @@ public async Task CheckRequirement(DiscountRuleVal
/// Discount identifier
/// Discount requirement identifier (if editing)
/// URL
- public string GetConfigurationUrl(string discountId, string discountRequirementId, string area = "Admin")
+ public string GetConfigurationUrl(string discountId, string discountRequirementId)
{
//configured
- var result = $"{area}/CustomerGroups/Configure/?discountId=" + discountId;
+ var result = $"/DiscountRulesCustomerGroups/Configure/?discountId=" + discountId;
if (!string.IsNullOrEmpty(discountRequirementId))
result += $"&discountRequirementId={discountRequirementId}";
return result;
diff --git a/src/Plugins/DiscountRules.Standard/Providers/HadSpentAmountDiscountRule.cs b/src/Plugins/DiscountRules.Standard/Providers/HadSpentAmountDiscountRule.cs
index 669b4aa4c..398948168 100644
--- a/src/Plugins/DiscountRules.Standard/Providers/HadSpentAmountDiscountRule.cs
+++ b/src/Plugins/DiscountRules.Standard/Providers/HadSpentAmountDiscountRule.cs
@@ -62,10 +62,10 @@ public async Task CheckRequirement(DiscountRuleVal
/// Discount identifier
/// Discount requirement identifier (if editing)
/// URL
- public string GetConfigurationUrl(string discountId, string discountRequirementId, string area = "Admin")
+ public string GetConfigurationUrl(string discountId, string discountRequirementId)
{
//configured
- var result = $"{area}/HadSpentAmount/Configure/?discountId=" + discountId;
+ var result = $"/DiscountRulesHadSpentAmount/Configure/?discountId=" + discountId;
if (!string.IsNullOrEmpty(discountRequirementId))
result += $"&discountRequirementId={discountRequirementId}";
return result;
diff --git a/src/Plugins/DiscountRules.Standard/Providers/HasAllProductsDiscountRule.cs b/src/Plugins/DiscountRules.Standard/Providers/HasAllProductsDiscountRule.cs
index 5ae2d0a94..89edd276b 100644
--- a/src/Plugins/DiscountRules.Standard/Providers/HasAllProductsDiscountRule.cs
+++ b/src/Plugins/DiscountRules.Standard/Providers/HasAllProductsDiscountRule.cs
@@ -121,10 +121,10 @@ into g
/// Discount identifier
/// Discount requirement identifier (if editing)
/// URL
- public string GetConfigurationUrl(string discountId, string discountRequirementId, string area = "Admin")
+ public string GetConfigurationUrl(string discountId, string discountRequirementId)
{
//configured
- var result = $"{area}/HasAllProducts/Configure/?discountId=" + discountId;
+ var result = $"/DiscountRulesHasAllProducts/Configure/?discountId=" + discountId;
if (!string.IsNullOrEmpty(discountRequirementId))
result += $"&discountRequirementId={discountRequirementId}";
return result;
diff --git a/src/Plugins/DiscountRules.Standard/Providers/HasOneProductDiscountRule.cs b/src/Plugins/DiscountRules.Standard/Providers/HasOneProductDiscountRule.cs
index 321ad12d7..5ad908b29 100644
--- a/src/Plugins/DiscountRules.Standard/Providers/HasOneProductDiscountRule.cs
+++ b/src/Plugins/DiscountRules.Standard/Providers/HasOneProductDiscountRule.cs
@@ -120,10 +120,10 @@ into g
/// Discount identifier
/// Discount requirement identifier (if editing)
/// URL
- public string GetConfigurationUrl(string discountId, string discountRequirementId, string area = "Admin")
+ public string GetConfigurationUrl(string discountId, string discountRequirementId)
{
//configured
- var result = $"{area}/HasOneProduct/Configure/?discountId=" + discountId;
+ var result = $"/DiscountRulesHasOneProduct/Configure/?discountId=" + discountId;
if (!string.IsNullOrEmpty(discountRequirementId))
result += $"&discountRequirementId={discountRequirementId}";
return result;
diff --git a/src/Plugins/DiscountRules.Standard/Providers/ShoppingCartDiscountRule.cs b/src/Plugins/DiscountRules.Standard/Providers/ShoppingCartDiscountRule.cs
index 43e08547a..9b584ee8a 100644
--- a/src/Plugins/DiscountRules.Standard/Providers/ShoppingCartDiscountRule.cs
+++ b/src/Plugins/DiscountRules.Standard/Providers/ShoppingCartDiscountRule.cs
@@ -83,10 +83,10 @@ public async Task CheckRequirement(DiscountRuleVal
/// Discount identifier
/// Discount requirement identifier (if editing)
/// URL
- public string GetConfigurationUrl(string discountId, string discountRequirementId, string area = "Admin")
+ public string GetConfigurationUrl(string discountId, string discountRequirementId)
{
//configured
- var result = $"{area}/ShoppingCartAmount/Configure/?discountId=" + discountId;
+ var result = $"/DiscountRulesShoppingCartAmount/Configure/?discountId=" + discountId;
if (!string.IsNullOrEmpty(discountRequirementId))
result += $"&discountRequirementId={discountRequirementId}";
return result;
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Admin/Views/CustomerGroups/Configure.cshtml b/src/Plugins/DiscountRules.Standard/Views/DiscountRulesCustomerGroups/Configure.cshtml
similarity index 96%
rename from src/Plugins/DiscountRules.Standard/Areas/Admin/Views/CustomerGroups/Configure.cshtml
rename to src/Plugins/DiscountRules.Standard/Views/DiscountRulesCustomerGroups/Configure.cshtml
index 6625a1b6f..af9bc57e5 100644
--- a/src/Plugins/DiscountRules.Standard/Areas/Admin/Views/CustomerGroups/Configure.cshtml
+++ b/src/Plugins/DiscountRules.Standard/Views/DiscountRulesCustomerGroups/Configure.cshtml
@@ -23,7 +23,7 @@
$.ajax({
cache:false,
type: "POST",
- url: "@Url.Action("Configure", "CustomerGroups")",
+ url: "@Url.Action("Configure", "DiscountRulesCustomerGroups")",
data: postData,
success: function (data) {
$('#pnl-save-requirement-result@(Model.RequirementId)').fadeIn("slow").delay(1000).fadeOut("slow");
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Admin/Views/HadSpentAmount/Configure.cshtml b/src/Plugins/DiscountRules.Standard/Views/DiscountRulesHadSpentAmount/Configure.cshtml
similarity index 96%
rename from src/Plugins/DiscountRules.Standard/Areas/Admin/Views/HadSpentAmount/Configure.cshtml
rename to src/Plugins/DiscountRules.Standard/Views/DiscountRulesHadSpentAmount/Configure.cshtml
index 1ac04e3dc..4000e88a4 100644
--- a/src/Plugins/DiscountRules.Standard/Areas/Admin/Views/HadSpentAmount/Configure.cshtml
+++ b/src/Plugins/DiscountRules.Standard/Views/DiscountRulesHadSpentAmount/Configure.cshtml
@@ -21,7 +21,7 @@
$.ajax({
cache:false,
type: "POST",
- url: "@Url.Action("Configure", "HadSpentAmount")",
+ url: "@Url.Action("Configure", "DiscountRulesHadSpentAmount")",
data: postData,
success: function (data) {
$('#pnl-save-requirement-result@(Model.RequirementId)').fadeIn("slow").delay(1000).fadeOut("slow");
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Admin/Views/HasAllProducts/Configure.cshtml b/src/Plugins/DiscountRules.Standard/Views/DiscountRulesHasAllProducts/Configure.cshtml
similarity index 98%
rename from src/Plugins/DiscountRules.Standard/Areas/Admin/Views/HasAllProducts/Configure.cshtml
rename to src/Plugins/DiscountRules.Standard/Views/DiscountRulesHasAllProducts/Configure.cshtml
index 4dc95039a..905d21e80 100644
--- a/src/Plugins/DiscountRules.Standard/Areas/Admin/Views/HasAllProducts/Configure.cshtml
+++ b/src/Plugins/DiscountRules.Standard/Views/DiscountRulesHasAllProducts/Configure.cshtml
@@ -21,7 +21,7 @@
$.ajax({
cache:false,
type: "POST",
- url: "@Url.Action("Configure", "HasAllProducts")",
+ url: "@Url.Action("Configure", "DiscountRulesHasAllProducts")",
data: postData,
success: function (data) {
$('#pnl-save-requirement-result@(Model.RequirementId)').fadeIn("slow").delay(1000).fadeOut("slow");
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Admin/Views/HasAllProducts/ProductAddPopup.cshtml b/src/Plugins/DiscountRules.Standard/Views/DiscountRulesHasAllProducts/ProductAddPopup.cshtml
similarity index 100%
rename from src/Plugins/DiscountRules.Standard/Areas/Admin/Views/HasAllProducts/ProductAddPopup.cshtml
rename to src/Plugins/DiscountRules.Standard/Views/DiscountRulesHasAllProducts/ProductAddPopup.cshtml
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Admin/Views/HasOneProduct/Configure.cshtml b/src/Plugins/DiscountRules.Standard/Views/DiscountRulesHasOneProduct/Configure.cshtml
similarity index 98%
rename from src/Plugins/DiscountRules.Standard/Areas/Admin/Views/HasOneProduct/Configure.cshtml
rename to src/Plugins/DiscountRules.Standard/Views/DiscountRulesHasOneProduct/Configure.cshtml
index 55b30a880..659ea5b18 100644
--- a/src/Plugins/DiscountRules.Standard/Areas/Admin/Views/HasOneProduct/Configure.cshtml
+++ b/src/Plugins/DiscountRules.Standard/Views/DiscountRulesHasOneProduct/Configure.cshtml
@@ -21,7 +21,7 @@
$.ajax({
cache:false,
type: "POST",
- url: "@Url.Action("Configure", "HasOneProduct")",
+ url: "@Url.Action("Configure", "DiscountRulesHasOneProduct")",
data: postData,
success: function (data) {
$('#pnl-save-requirement-result@(Model.RequirementId)').fadeIn("slow").delay(1000).fadeOut("slow");
diff --git a/src/Plugins/DiscountRules.Standard/Areas/Admin/Views/HasOneProduct/ProductAddPopup.cshtml b/src/Plugins/DiscountRules.Standard/Views/DiscountRulesHasOneProduct/ProductAddPopup.cshtml
similarity index 98%
rename from src/Plugins/DiscountRules.Standard/Areas/Admin/Views/HasOneProduct/ProductAddPopup.cshtml
rename to src/Plugins/DiscountRules.Standard/Views/DiscountRulesHasOneProduct/ProductAddPopup.cshtml
index fc917a7ee..9c4fcbb07 100644
--- a/src/Plugins/DiscountRules.Standard/Areas/Admin/Views/HasOneProduct/ProductAddPopup.cshtml
+++ b/src/Plugins/DiscountRules.Standard/Views/DiscountRulesHasOneProduct/ProductAddPopup.cshtml
@@ -8,7 +8,7 @@
//page title
ViewBag.Title = Loc["Plugins.DiscountRules.HasOneProduct.Fields.Products.Choose"];
}
-