From 228641aa8884f1539c4ac24d41b79d4a3afbac15 Mon Sep 17 00:00:00 2001 From: KrzysztofPajak Date: Wed, 10 Jun 2026 21:10:37 +0200 Subject: [PATCH 1/5] Add store-scoped payment management to Grand.Web.Store Adds a PaymentController to the store-manager panel mirroring the Admin payment configuration, scoped to the store owner (CurrentStoreId), following the existing Store ShippingController conventions: - Payment methods: list providers and toggle active per store - Payment settings: store-scoped PaymentSettings toggles - Payment restrictions: by country / shipping method, per store Payment restrictions were stored globally (setting keys PaymentMethodRestictions.{SystemName} / ...Shipping.{SystemName} with no storeId). To support true per-store isolation, the four restriction methods on IPaymentService/PaymentService now take an optional storeId (default ""), which is threaded into ISettingService (store-specific read with global fallback, per-store write) and into LoadActive/LoadAllPaymentMethods so restrictions apply per store at checkout. The default keeps Admin behaviour unchanged. Plugin configuration links are intentionally deferred to a separate task. Co-Authored-By: Claude Opus 4.8 --- .../Services/Payments/PaymentService.cs | 20 +- .../Checkout/Payments/IPaymentService.cs | 12 +- .../Areas/Store/Views/Payment/Index.cshtml | 141 +++++++++++++ .../Views/Payment/MethodRestrictions.cshtml | 46 ++++ .../MethodRestrictions.TabCountry.cshtml | 81 +++++++ .../MethodRestrictions.TabShipping.cshtml | 80 +++++++ .../Areas/Store/Views/Payment/Settings.cshtml | 95 +++++++++ .../Areas/Store/Views/_ViewImports.cshtml | 1 + .../Controllers/PaymentController.cs | 199 ++++++++++++++++++ 9 files changed, 661 insertions(+), 14 deletions(-) create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/Payment/Index.cshtml create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/Payment/MethodRestrictions.cshtml create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/Payment/Partials/MethodRestrictions.TabCountry.cshtml create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/Payment/Partials/MethodRestrictions.TabShipping.cshtml create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/Payment/Settings.cshtml create mode 100644 src/Web/Grand.Web.Store/Controllers/PaymentController.cs diff --git a/src/Business/Grand.Business.Checkout/Services/Payments/PaymentService.cs b/src/Business/Grand.Business.Checkout/Services/Payments/PaymentService.cs index 78abb1d21..0c2b91862 100644 --- a/src/Business/Grand.Business.Checkout/Services/Payments/PaymentService.cs +++ b/src/Business/Grand.Business.Checkout/Services/Payments/PaymentService.cs @@ -69,7 +69,7 @@ public virtual async Task> LoadActivePaymentMethods(Cust if (selectedShippingOption == null) return await Task.FromResult(pm); for (var i = pm.Count - 1; i >= 0; i--) { - var restrictedGroupIds =await GetRestrictedShippingIds(pm[i]); + var restrictedGroupIds =await GetRestrictedShippingIds(pm[i], storeId); if (restrictedGroupIds.Contains(selectedShippingOption.Name)) pm.Remove(pm[i]); } @@ -109,7 +109,7 @@ public virtual async Task> LoadAllPaymentMethods(Custome var filteredPaymentMethods = new List(); foreach (var pm in paymentMethods) { - var restrictedCountryIds = await GetRestrictedCountryIds(pm); + var restrictedCountryIds = await GetRestrictedCountryIds(pm, storeId); if (!restrictedCountryIds.Contains(filterByCountryId)) { filteredPaymentMethods.Add(pm); @@ -123,12 +123,12 @@ public virtual async Task> LoadAllPaymentMethods(Custome /// /// Payment method /// A list of country identifiers - public virtual async Task> GetRestrictedCountryIds(IPaymentProvider paymentMethod) + public virtual async Task> GetRestrictedCountryIds(IPaymentProvider paymentMethod, string storeId = "") { ArgumentNullException.ThrowIfNull(paymentMethod); var settingKey = $"PaymentMethodRestictions.{paymentMethod.SystemName}"; - var restrictedCountryIds = await _settingService.GetSettingByKey(settingKey); + var restrictedCountryIds = await _settingService.GetSettingByKey(settingKey, storeId: storeId); return restrictedCountryIds == null ? new List() : restrictedCountryIds.Ids; } @@ -137,12 +137,12 @@ public virtual async Task> GetRestrictedCountryIds(IPaymentProvide /// /// Payment method /// A list of role identifiers - public virtual async Task> GetRestrictedShippingIds(IPaymentProvider paymentMethod) + public virtual async Task> GetRestrictedShippingIds(IPaymentProvider paymentMethod, string storeId = "") { ArgumentNullException.ThrowIfNull(paymentMethod); var settingKey = $"PaymentMethodRestictionsShipping.{paymentMethod.SystemName}"; - var restrictedShippingIds = await _settingService.GetSettingByKey(settingKey); + var restrictedShippingIds = await _settingService.GetSettingByKey(settingKey, storeId: storeId); return restrictedShippingIds == null ? new List() : restrictedShippingIds.Ids; } @@ -151,12 +151,12 @@ public virtual async Task> GetRestrictedShippingIds(IPaymentProvid /// /// Payment method /// A list of country identifiers - public virtual async Task SaveRestrictedCountryIds(IPaymentProvider paymentMethod, List countryIds) + public virtual async Task SaveRestrictedCountryIds(IPaymentProvider paymentMethod, List countryIds, string storeId = "") { ArgumentNullException.ThrowIfNull(paymentMethod); var settingKey = $"PaymentMethodRestictions.{paymentMethod.SystemName}"; - await _settingService.SetSetting(settingKey, new PaymentRestrictedSettings { Ids = countryIds }); + await _settingService.SetSetting(settingKey, new PaymentRestrictedSettings { Ids = countryIds }, storeId); } /// @@ -177,12 +177,12 @@ public virtual async Task SaveRestrictedGroupIds(IPaymentProvider paymentMethod, /// /// Payment method /// A list of country identifiers - public virtual async Task SaveRestrictedShippingIds(IPaymentProvider paymentMethod, List shippingIds) + public virtual async Task SaveRestrictedShippingIds(IPaymentProvider paymentMethod, List shippingIds, string storeId = "") { ArgumentNullException.ThrowIfNull(paymentMethod); var settingKey = $"PaymentMethodRestictionsShipping.{paymentMethod.SystemName}"; - await _settingService.SetSetting(settingKey, new PaymentRestrictedSettings { Ids = shippingIds }); + await _settingService.SetSetting(settingKey, new PaymentRestrictedSettings { Ids = shippingIds }, storeId); } /// diff --git a/src/Business/Grand.Business.Core/Interfaces/Checkout/Payments/IPaymentService.cs b/src/Business/Grand.Business.Core/Interfaces/Checkout/Payments/IPaymentService.cs index 5f35ac822..838a1e4ea 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Checkout/Payments/IPaymentService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Checkout/Payments/IPaymentService.cs @@ -42,29 +42,33 @@ Task> LoadAllPaymentMethods(Customer customer = null, st /// Gets a list of country identifiers in which a certain payment method is now allowed /// /// Payment method + /// Store ident; pass "" for the global restriction /// A list of country identifiers - Task> GetRestrictedCountryIds(IPaymentProvider paymentMethod); + Task> GetRestrictedCountryIds(IPaymentProvider paymentMethod, string storeId = ""); /// /// Gets a list of shipping identifiers in which a certain payment method is now allowed /// /// Payment method + /// Store ident; pass "" for the global restriction /// A list of role identifiers - Task> GetRestrictedShippingIds(IPaymentProvider paymentMethod); + Task> GetRestrictedShippingIds(IPaymentProvider paymentMethod, string storeId = ""); /// /// Saves a list of country identifiers in which a certain payment method is now allowed /// /// Payment method /// A list of country identifiers - Task SaveRestrictedCountryIds(IPaymentProvider paymentMethod, List countryIds); + /// Store ident; pass "" for the global restriction + Task SaveRestrictedCountryIds(IPaymentProvider paymentMethod, List countryIds, string storeId = ""); /// /// Saves a list of shipping identifiers in which a certain payment method is now allowed /// /// Payment method /// A list of shipping identifiers - Task SaveRestrictedShippingIds(IPaymentProvider paymentMethod, List shippingIds); + /// Store ident; pass "" for the global restriction + Task SaveRestrictedShippingIds(IPaymentProvider paymentMethod, List shippingIds, string storeId = ""); /// /// Process a payment diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Payment/Index.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Payment/Index.cshtml new file mode 100644 index 000000000..c7e50fb35 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Payment/Index.cshtml @@ -0,0 +1,141 @@ +@{ + ViewBag.Title = Loc["Admin.Configuration.Payment.Methods"]; + Layout = Constants.LayoutStore; +} + +
+
+
+
+
+ + @Loc["Admin.Configuration.Payment.Methods"] +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Payment/MethodRestrictions.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Payment/MethodRestrictions.cshtml new file mode 100644 index 000000000..926437da5 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Payment/MethodRestrictions.cshtml @@ -0,0 +1,46 @@ +@model PaymentMethodRestrictionModel + +@{ + ViewBag.Title = Loc["Admin.Configuration.Payment.MethodRestrictions"]; + Layout = Constants.LayoutStore; +} +
+ +
+
+
+
+
+ + @Loc["Admin.Configuration.Payment.MethodRestrictions"] +
+
+ +
+
+
+ + + + +
+ +
+
+
+ + +
+ +
+
+
+
+
+
+
+
+
+
diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Payment/Partials/MethodRestrictions.TabCountry.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Payment/Partials/MethodRestrictions.TabCountry.cshtml new file mode 100644 index 000000000..87c91f0bd --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Payment/Partials/MethodRestrictions.TabCountry.cshtml @@ -0,0 +1,81 @@ +@model PaymentMethodRestrictionModel +
+
+
+
+
+ @Loc["Admin.Configuration.Payment.MethodRestrictions.DescriptionCountry"] +
+ @if (Model.AvailablePaymentMethods.Count == 0) + { + No payment methods installed + } + else if (Model.AvailableCountries.Count == 0) + { + No countries available + } + else + { + + + + + + @foreach (var pm in Model.AvailablePaymentMethods) + { + var systemNameWithoutDot = pm.SystemName.Replace(".", ""); + + } + + @{ + var altRow = true; + } + @foreach (var c in Model.AvailableCountries) + { + altRow = !altRow; + + + @foreach (var pm in Model.AvailablePaymentMethods) + { + var resticted = Model.Resticted.ContainsKey(pm.SystemName) && Model.Resticted[pm.SystemName][c.Id]; + + var systemNameWithoutDot = pm.SystemName.Replace(".", ""); + + } + + } + +
+ @Loc["Admin.Configuration.Payment.MethodRestrictions.Country"] + + @pm.FriendlyName + +
+ @c.Name + + +
+ } +
+
+
+
diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Payment/Partials/MethodRestrictions.TabShipping.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Payment/Partials/MethodRestrictions.TabShipping.cshtml new file mode 100644 index 000000000..d4f704274 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Payment/Partials/MethodRestrictions.TabShipping.cshtml @@ -0,0 +1,80 @@ +@model PaymentMethodRestrictionModel +
+
+
+
+
+ @Loc["Admin.Configuration.Payment.MethodRestrictions.DescriptionShipping"] +
+ @if (Model.AvailablePaymentMethods.Count == 0) + { + No payment methods installed + } + else if (Model.AvailableShippingMethods.Count == 0) + { + No shipping method available + } + else + { + + + + + + @foreach (var pm in Model.AvailablePaymentMethods) + { + var systemNameWithoutDot = pm.SystemName.Replace(".", ""); + + } + + @{ + var altRow = true; + } + @foreach (var c in Model.AvailableShippingMethods) + { + altRow = !altRow; + + + @foreach (var pm in Model.AvailablePaymentMethods) + { + var resticted = Model.RestictedShipping.ContainsKey(pm.SystemName) && Model.RestictedShipping[pm.SystemName][c.Name]; + var systemNameWithoutDot = pm.SystemName.Replace(".", ""); + + } + + } + +
+ @Loc["Admin.Configuration.Payment.MethodRestrictions.Shipping"] + + @pm.FriendlyName + +
+ @c.Name + + +
+ } +
+
+
+
diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Payment/Settings.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Payment/Settings.cshtml new file mode 100644 index 000000000..c71c8a8c4 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Payment/Settings.cshtml @@ -0,0 +1,95 @@ +@model PaymentSettingsModel +@{ + ViewBag.Title = Loc["Admin.Configuration.Payment.Settings"]; + Layout = Constants.LayoutStore; +} +
+ +
+ +
+
+
+
+
+ + @Loc["Admin.Configuration.Payment.Settings"] +
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+ + +
+
+
+
+ +
+
+ + +
+
+
+
+ +
+
+ + +
+
+
+
+ +
+
+ + +
+
+
+
+ +
+
+ + +
+
+
+
+
+
+
+
+
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 c34fda895..38a41e485 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.Payments @using Grand.Web.AdminShared.Models.Discounts @using Grand.Web.AdminShared.Models.Tax @using Grand.Web.Store.Models.Messages diff --git a/src/Web/Grand.Web.Store/Controllers/PaymentController.cs b/src/Web/Grand.Web.Store/Controllers/PaymentController.cs new file mode 100644 index 000000000..ff597b5bf --- /dev/null +++ b/src/Web/Grand.Web.Store/Controllers/PaymentController.cs @@ -0,0 +1,199 @@ +using Grand.Business.Core.Extensions; +using Grand.Business.Core.Interfaces.Checkout.Payments; +using Grand.Business.Core.Interfaces.Checkout.Shipping; +using Grand.Business.Core.Interfaces.Common.Configuration; +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Domain.Payments; +using Grand.Domain.Permissions; +using Grand.Infrastructure; +using Grand.Web.AdminShared.Extensions.Mapping; +using Grand.Web.AdminShared.Extensions.Mapping.Settings; +using Grand.Web.AdminShared.Models.Payments; +using Grand.Web.AdminShared.Models.Shipping; +using Grand.Web.Common.DataSource; +using Grand.Web.Common.Security.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Grand.Web.Store.Controllers; + +[PermissionAuthorize(PermissionSystemName.PaymentMethods)] +public class PaymentController( + IPaymentService paymentService, + ISettingService settingService, + ICountryService countryService, + IShippingMethodService shippingMethodService, + ITranslationService translationService, + IContextAccessor contextAccessor) : BaseStoreController +{ + private string CurrentStoreId => contextAccessor.WorkContext.CurrentCustomer.StaffStoreId; + + #region Payment methods + + public IActionResult Index() + { + return View(); + } + + [HttpPost] + [PermissionAuthorizeAction(PermissionActionName.List)] + public async Task Methods() + { + var paymentSettings = await settingService.LoadSetting(CurrentStoreId); + + var paymentMethodsModel = new List(); + var paymentMethods = await paymentService.LoadAllPaymentMethods(storeId: CurrentStoreId); + foreach (var paymentMethod in paymentMethods) + { + var tmp = await paymentMethod.ToModel(); + tmp.IsActive = paymentMethod.IsPaymentMethodActive(paymentSettings); + paymentMethodsModel.Add(tmp); + } + + var gridModel = new DataSourceResult { + Data = paymentMethodsModel, + Total = paymentMethodsModel.Count + }; + + return Json(gridModel); + } + + [HttpPost] + [PermissionAuthorizeAction(PermissionActionName.Edit)] + public async Task MethodUpdate(PaymentMethodModel model) + { + var paymentSettings = await settingService.LoadSetting(CurrentStoreId); + + var pm = paymentService.LoadPaymentMethodBySystemName(model.SystemName); + if (pm == null) + return new JsonResult(""); + + if (pm.IsPaymentMethodActive(paymentSettings)) + { + if (!model.IsActive) + { + paymentSettings.ActivePaymentProviderSystemNames.Remove(pm.SystemName); + await settingService.SaveSetting(paymentSettings, CurrentStoreId); + } + } + else + { + if (model.IsActive) + { + paymentSettings.ActivePaymentProviderSystemNames.Add(pm.SystemName); + await settingService.SaveSetting(paymentSettings, CurrentStoreId); + } + } + + return new JsonResult(""); + } + + #endregion + + #region Payment settings + + public async Task Settings() + { + var paymentSettings = await settingService.LoadSetting(CurrentStoreId); + var model = paymentSettings.ToModel(); + return View(model); + } + + [HttpPost] + public async Task Settings(PaymentSettingsModel model) + { + var paymentSettings = await settingService.LoadSetting(CurrentStoreId); + paymentSettings = model.ToEntity(paymentSettings); + await settingService.SaveSetting(paymentSettings, CurrentStoreId); + + Success(translationService.GetResource("Admin.Configuration.Updated")); + return RedirectToAction("Settings"); + } + + #endregion + + #region Restrictions + + [PermissionAuthorizeAction(PermissionActionName.Preview)] + public async Task MethodRestrictions() + { + var model = new PaymentMethodRestrictionModel(); + var paymentMethods = await paymentService.LoadAllPaymentMethods(storeId: CurrentStoreId); + var countries = await countryService.GetAllCountries(showHidden: true); + var shippings = await shippingMethodService.GetAllShippingMethods(CurrentStoreId); + + foreach (var pm in paymentMethods) model.AvailablePaymentMethods.Add(await pm.ToModel()); + foreach (var c in countries) model.AvailableCountries.Add(c.ToModel()); + foreach (var s in shippings) + model.AvailableShippingMethods.Add(new ShippingMethodModel { + Id = s.Id, + Name = s.Name + }); + + foreach (var pm in paymentMethods) + { + var restictedCountries = await paymentService.GetRestrictedCountryIds(pm, CurrentStoreId); + foreach (var c in countries) + { + var resticted = restictedCountries.Contains(c.Id); + if (!model.Resticted.ContainsKey(pm.SystemName)) + model.Resticted[pm.SystemName] = new Dictionary(); + model.Resticted[pm.SystemName][c.Id] = resticted; + } + + var restictedShipping = await paymentService.GetRestrictedShippingIds(pm, CurrentStoreId); + foreach (var s in shippings) + { + var resticted = restictedShipping.Contains(s.Name); + if (!model.RestictedShipping.ContainsKey(pm.SystemName)) + model.RestictedShipping[pm.SystemName] = new Dictionary(); + model.RestictedShipping[pm.SystemName][s.Name] = resticted; + } + } + + return View(model); + } + + [HttpPost] + [ActionName("MethodRestrictions")] + [RequestFormLimits(ValueCountLimit = 2048)] + [PermissionAuthorizeAction(PermissionActionName.Edit)] + public async Task MethodRestrictionsSave(IDictionary model) + { + var paymentMethods = await paymentService.LoadAllPaymentMethods(storeId: CurrentStoreId); + var countries = await countryService.GetAllCountries(showHidden: true); + var shippings = await shippingMethodService.GetAllShippingMethods(CurrentStoreId); + + foreach (var pm in paymentMethods) + { + if (model.TryGetValue($"restrict_{pm.SystemName.Replace(".", "")}", out var countryIds)) + { + var countryIdsToRestrict = countryIds.ToList(); + var newCountryIds = + (from c in countries where countryIdsToRestrict.Contains(c.Id) select c.Id).ToList(); + await paymentService.SaveRestrictedCountryIds(pm, newCountryIds, CurrentStoreId); + } + else + { + await paymentService.SaveRestrictedCountryIds(pm, new List(), CurrentStoreId); + } + + if (model.TryGetValue($"restrictship_{pm.SystemName.Replace(".", "")}", out var shipIds)) + { + var shipIdsToRestrict = shipIds.ToList(); + var newShipIds = (from s in shippings where shipIdsToRestrict.Contains(s.Name) select s.Name).ToList(); + await paymentService.SaveRestrictedShippingIds(pm, newShipIds, CurrentStoreId); + } + else + { + await paymentService.SaveRestrictedShippingIds(pm, new List(), CurrentStoreId); + } + } + + Success(translationService.GetResource("Admin.Configuration.Payment.MethodRestrictions.Updated")); + await SaveSelectedTabIndex(); + return RedirectToAction("MethodRestrictions"); + } + + #endregion +} From c251ec7a0ea0d6f2a5c60202dd5179f591102432 Mon Sep 17 00:00:00 2001 From: KrzysztofPajak Date: Thu, 11 Jun 2026 20:22:59 +0200 Subject: [PATCH 2/5] Further changes --- .../Views/Payment/MethodRestrictions.cshtml | 7 +++++++ .../Controllers/PaymentController.cs | 16 ++++++++++------ .../Controllers/PaymentController.cs | 4 ++-- .../Controllers/ShippingController.cs | 6 ++---- 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Payment/MethodRestrictions.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Payment/MethodRestrictions.cshtml index cc8e28179..740e9b75d 100644 --- a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Payment/MethodRestrictions.cshtml +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Payment/MethodRestrictions.cshtml @@ -20,6 +20,13 @@ +
+
+
+ @await Component.InvokeAsync("StoreScope") +
+
+
diff --git a/src/Web/Grand.Web.Admin/Controllers/PaymentController.cs b/src/Web/Grand.Web.Admin/Controllers/PaymentController.cs index c552cd35f..60669eaff 100644 --- a/src/Web/Grand.Web.Admin/Controllers/PaymentController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/PaymentController.cs @@ -147,6 +147,8 @@ public async Task ConfigureMethod(string systemName) public async Task MethodRestrictions() { + var storeScope = await GetActiveStore(); + var model = new PaymentMethodRestrictionModel(); var paymentMethods = await _paymentService.LoadAllPaymentMethods(); var countries = await _countryService.GetAllCountries(showHidden: true); @@ -162,7 +164,7 @@ public async Task MethodRestrictions() foreach (var pm in paymentMethods) { - var restictedCountries = await _paymentService.GetRestrictedCountryIds(pm); + var restictedCountries = await _paymentService.GetRestrictedCountryIds(pm, storeScope); foreach (var c in countries) { var resticted = restictedCountries.Contains(c.Id); @@ -171,7 +173,7 @@ public async Task MethodRestrictions() model.Resticted[pm.SystemName][c.Id] = resticted; } - var restictedShipping = await _paymentService.GetRestrictedShippingIds(pm); + var restictedShipping = await _paymentService.GetRestrictedShippingIds(pm, storeScope); foreach (var s in shippings) { var resticted = restictedShipping.Contains(s.Name); @@ -189,6 +191,8 @@ public async Task MethodRestrictions() [RequestFormLimits(ValueCountLimit = 2048)] public async Task MethodRestrictionsSave(IDictionary model) { + var storeScope = await GetActiveStore(); + var paymentMethods = await _paymentService.LoadAllPaymentMethods(); var countries = await _countryService.GetAllCountries(showHidden: true); var shippings = await _shippingMethodService.GetAllShippingMethods(); @@ -200,22 +204,22 @@ public async Task MethodRestrictionsSave(IDictionary()); + await _paymentService.SaveRestrictedCountryIds(pm, new List(), storeScope); } if (model.TryGetValue($"restrictship_{pm.SystemName.Replace(".", "")}", out var shipIds)) { var shipIdsToRestrict = shipIds.ToList(); var newShipIds = (from s in shippings where shipIdsToRestrict.Contains(s.Name) select s.Name).ToList(); - await _paymentService.SaveRestrictedShippingIds(pm, newShipIds); + await _paymentService.SaveRestrictedShippingIds(pm, newShipIds, storeScope); } else { - await _paymentService.SaveRestrictedShippingIds(pm, new List()); + await _paymentService.SaveRestrictedShippingIds(pm, new List(), storeScope); } } diff --git a/src/Web/Grand.Web.Store/Controllers/PaymentController.cs b/src/Web/Grand.Web.Store/Controllers/PaymentController.cs index ff597b5bf..770596359 100644 --- a/src/Web/Grand.Web.Store/Controllers/PaymentController.cs +++ b/src/Web/Grand.Web.Store/Controllers/PaymentController.cs @@ -120,7 +120,7 @@ public async Task MethodRestrictions() var model = new PaymentMethodRestrictionModel(); var paymentMethods = await paymentService.LoadAllPaymentMethods(storeId: CurrentStoreId); var countries = await countryService.GetAllCountries(showHidden: true); - var shippings = await shippingMethodService.GetAllShippingMethods(CurrentStoreId); + var shippings = await shippingMethodService.GetAllShippingMethods(storeId: CurrentStoreId); foreach (var pm in paymentMethods) model.AvailablePaymentMethods.Add(await pm.ToModel()); foreach (var c in countries) model.AvailableCountries.Add(c.ToModel()); @@ -162,7 +162,7 @@ public async Task MethodRestrictionsSave(IDictionary Restrictions() var model = new ShippingMethodRestrictionModel(); var countries = await countryService.GetAllCountries(showHidden: true); - var shippingMethods = (await shippingMethodService.GetAllShippingMethods(CurrentStoreId)) - .Where(sm => sm.StoreId == CurrentStoreId).ToList(); + var shippingMethods = await shippingMethodService.GetAllShippingMethods(storeId: CurrentStoreId); var customerGroups = await groupService.GetAllCustomerGroups(); foreach (var country in countries) @@ -636,8 +635,7 @@ public async Task Restrictions() public async Task RestrictionSave(IDictionary model) { var countries = await countryService.GetAllCountries(showHidden: true); - var shippingMethods = (await shippingMethodService.GetAllShippingMethods(CurrentStoreId)) - .Where(sm => sm.StoreId == CurrentStoreId).ToList(); + var shippingMethods = await shippingMethodService.GetAllShippingMethods(storeId: CurrentStoreId); var customerGroups = await groupService.GetAllCustomerGroups(); foreach (var shippingMethod in shippingMethods) { From 4a22161471f89b14ccded825b837676bc8ed8d7c Mon Sep 17 00:00:00 2001 From: KrzysztofPajak Date: Thu, 11 Jun 2026 20:34:59 +0200 Subject: [PATCH 3/5] Commit --- .../Services/ProductViewModelService.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Web/Grand.Web.AdminShared/Services/ProductViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/ProductViewModelService.cs index 8a61e1b01..426490681 100644 --- a/src/Web/Grand.Web.AdminShared/Services/ProductViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/ProductViewModelService.cs @@ -366,7 +366,7 @@ public virtual async Task PrepareProductModel(ProductModel model, Product produc } //tax categories - var taxCategories = await taxCategoryService.GetAllTaxCategories(); + var taxCategories = await taxCategoryService.GetAllTaxCategories(contextAccessor.WorkContext.CurrentCustomer.StaffStoreId); model.AvailableTaxCategories.Add(new SelectListItem { Text = translationService.GetResource("Admin.Configuration.Tax.Settings.TaxCategories.None"), Value = "" @@ -400,7 +400,7 @@ public virtual async Task PrepareProductModel(ProductModel model, Product produc model.AvailableUnits.Add(new SelectListItem { Text = un.Name, Value = un.Id, Selected = product != null && un.Id == product.UnitId }); //discounts - model.AvailableDiscounts = (await discountService.GetDiscountsQuery(DiscountType.AssignedToSkus, model.StoreId)) + model.AvailableDiscounts = (await discountService.GetDiscountsQuery(DiscountType.AssignedToSkus, contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) .Select(d => d.ToModel(dateTimeService)) .ToList(); if (!excludeProperties && product != null) model.SelectedDiscountIds = product.AppliedDiscounts.ToArray(); @@ -529,7 +529,7 @@ public virtual async Task PrepareProductListModel(string store model.AvailableStores.Add(new SelectListItem { Text = s.Shortcut, Value = s.Id }); //warehouses model.AvailableWarehouses.Add(new SelectListItem { Text = translationService.GetResource("Admin.Common.All"), Value = " " }); - foreach (var wh in await warehouseService.GetAllWarehouses()) + foreach (var wh in await warehouseService.GetAllWarehouses(storeId)) model.AvailableWarehouses.Add(new SelectListItem { Text = wh.Name, Value = wh.Id }); //product types From 65968c065f8a12f13c4e970fd23c0f61fcec3c9c Mon Sep 17 00:00:00 2001 From: KrzysztofPajak Date: Fri, 12 Jun 2026 19:59:39 +0200 Subject: [PATCH 4/5] Add tests --- GrandNode.sln | 28 +- .../Services/Orders/LoyaltyPointsService.cs | 5 +- .../Services/Payments/PaymentServiceTests.cs | 113 ++++++++ .../Controllers/PaymentControllerTests.cs | 174 +++++++++++++ .../Services/ProductViewModelServiceTests.cs | 206 +++++++++++++++ .../Controllers/PaymentControllerTests.cs | 246 ++++++++++++++++++ .../Controllers/ShippingControllerTests.cs | 153 +++++++++++ .../Grand.Web.Store.Tests.csproj | 24 ++ 8 files changed, 946 insertions(+), 3 deletions(-) create mode 100644 src/Tests/Grand.Web.Admin.Tests/Controllers/PaymentControllerTests.cs create mode 100644 src/Tests/Grand.Web.Admin.Tests/Services/ProductViewModelServiceTests.cs create mode 100644 src/Tests/Grand.Web.Store.Tests/Controllers/PaymentControllerTests.cs create mode 100644 src/Tests/Grand.Web.Store.Tests/Controllers/ShippingControllerTests.cs create mode 100644 src/Tests/Grand.Web.Store.Tests/Grand.Web.Store.Tests.csproj diff --git a/GrandNode.sln b/GrandNode.sln index c7d8738dc..3b6f0c8d0 100644 --- a/GrandNode.sln +++ b/GrandNode.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 -VisualStudioVersion = 18.5.11619.145 insiders +VisualStudioVersion = 18.5.11619.145 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{FA350BD6-C29D-40D9-BA47-FE5FBDFC84A0}" EndProject @@ -149,6 +149,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Grand.Web.AdminShared", "sr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Grand.Mapping.Tests", "src\Tests\Grand.Mapping.Tests\Grand.Mapping.Tests.csproj", "{396E6929-5365-4116-8AA8-DF5327247798}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{CEA09484-30F6-4D44-02F6-822E06DBC57C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Grand.Web.Store.Tests", "src\Tests\Grand.Web.Store.Tests\Grand.Web.Store.Tests.csproj", "{5819B37B-4972-4BBC-B51C-57B0028EF869}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{8D626EA8-CB54-BC41-363A-217881BEBA6E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Web", "Web", "{03997797-E7F5-0643-168D-B8EA7178C2FE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -891,6 +901,18 @@ Global {396E6929-5365-4116-8AA8-DF5327247798}.Release|x64.Build.0 = Release|Any CPU {396E6929-5365-4116-8AA8-DF5327247798}.Release|x86.ActiveCfg = Release|Any CPU {396E6929-5365-4116-8AA8-DF5327247798}.Release|x86.Build.0 = Release|Any CPU + {5819B37B-4972-4BBC-B51C-57B0028EF869}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5819B37B-4972-4BBC-B51C-57B0028EF869}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5819B37B-4972-4BBC-B51C-57B0028EF869}.Debug|x64.ActiveCfg = Debug|Any CPU + {5819B37B-4972-4BBC-B51C-57B0028EF869}.Debug|x64.Build.0 = Debug|Any CPU + {5819B37B-4972-4BBC-B51C-57B0028EF869}.Debug|x86.ActiveCfg = Debug|Any CPU + {5819B37B-4972-4BBC-B51C-57B0028EF869}.Debug|x86.Build.0 = Debug|Any CPU + {5819B37B-4972-4BBC-B51C-57B0028EF869}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5819B37B-4972-4BBC-B51C-57B0028EF869}.Release|Any CPU.Build.0 = Release|Any CPU + {5819B37B-4972-4BBC-B51C-57B0028EF869}.Release|x64.ActiveCfg = Release|Any CPU + {5819B37B-4972-4BBC-B51C-57B0028EF869}.Release|x64.Build.0 = Release|Any CPU + {5819B37B-4972-4BBC-B51C-57B0028EF869}.Release|x86.ActiveCfg = Release|Any CPU + {5819B37B-4972-4BBC-B51C-57B0028EF869}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -956,6 +978,10 @@ Global {136F1E35-8B20-465C-8D42-30A5A0D0DA1F} = {583FFBBD-421C-4FB7-BBDA-F9CAF35A354A} {12C4A556-E62E-4EC0-BCD9-E9D4E1F3D430} = {BF4543A8-0731-4FDD-BB6B-0ADB9561F981} {396E6929-5365-4116-8AA8-DF5327247798} = {6360202A-F931-4BBD-ADBD-C9A628EE59F8} + {CEA09484-30F6-4D44-02F6-822E06DBC57C} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {5819B37B-4972-4BBC-B51C-57B0028EF869} = {CEA09484-30F6-4D44-02F6-822E06DBC57C} + {8D626EA8-CB54-BC41-363A-217881BEBA6E} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {03997797-E7F5-0643-168D-B8EA7178C2FE} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {88B478F4-FD3B-4C24-9E84-4FAAF0254397} diff --git a/src/Business/Grand.Business.Checkout/Services/Orders/LoyaltyPointsService.cs b/src/Business/Grand.Business.Checkout/Services/Orders/LoyaltyPointsService.cs index 526811426..b2a693a08 100644 --- a/src/Business/Grand.Business.Checkout/Services/Orders/LoyaltyPointsService.cs +++ b/src/Business/Grand.Business.Checkout/Services/Orders/LoyaltyPointsService.cs @@ -55,7 +55,8 @@ public virtual async Task GetLoyaltyPointsBalance(string customerId, string query = query.Where(rph => rph.CustomerId == customerId); if (!_loyaltyPointsSettings.PointsAccumulatedForAllStores) query = query.Where(rph => rph.StoreId == storeId); - query = query.OrderByDescending(rph => rph.CreatedOnUtc); + //Id as tie-breaker - entries created within the same millisecond share CreatedOnUtc + query = query.OrderByDescending(rph => rph.CreatedOnUtc).ThenByDescending(rph => rph.Id); var lastRph = await Task.FromResult(query.FirstOrDefault()); return lastRph?.PointsBalance ?? 0; @@ -104,7 +105,7 @@ public virtual async Task> GetLoyaltyPointsHistory(s //filter by store if (!string.IsNullOrEmpty(storeId)) query = query.Where(rph => rph.StoreId == storeId); - query = query.OrderByDescending(rph => rph.CreatedOnUtc); + query = query.OrderByDescending(rph => rph.CreatedOnUtc).ThenByDescending(rph => rph.Id); return await Task.FromResult(query.ToList()); } diff --git a/src/Tests/Grand.Business.Checkout.Tests/Services/Payments/PaymentServiceTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Services/Payments/PaymentServiceTests.cs index 517e4a3b5..e761b10bf 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Services/Payments/PaymentServiceTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Services/Payments/PaymentServiceTests.cs @@ -68,6 +68,63 @@ public async Task GetRestrictedCountryIds_ReturnEmptyList() _settingService.Verify(s => s.GetSettingByKey(expectedKey, null, ""), Times.Once); } + [TestMethod] + public async Task GetRestrictedCountryIds_WithStoreId_PassStoreIdToSettingService() + { + _paymentProviderMock.Setup(c => c.SystemName).Returns("systemName"); + var expectedResult = new List { "1", "2" }; + var expectedKey = "PaymentMethodRestictions.systemName"; + _settingService.Setup(s => s.GetSettingByKey(It.IsAny(), null, "storeId")) + .Returns(() => Task.FromResult(new PaymentRestrictedSettings { Ids = expectedResult })); + + var result = await _paymentService.GetRestrictedCountryIds(_paymentProviderMock.Object, "storeId"); + Assert.IsTrue(expectedResult.SequenceEqual(result)); + _settingService.Verify(s => s.GetSettingByKey(expectedKey, null, "storeId"), + Times.Once); + } + + [TestMethod] + public async Task GetRestrictedShippingIds_ReturnExpectedIds() + { + _paymentProviderMock.Setup(c => c.SystemName).Returns("systemName"); + var expectedResult = new List { "Ground", "Pickup" }; + var expectedKey = "PaymentMethodRestictionsShipping.systemName"; + _settingService.Setup(s => s.GetSettingByKey(It.IsAny(), null, "")) + .Returns(() => Task.FromResult(new PaymentRestrictedSettings { Ids = expectedResult })); + + var result = await _paymentService.GetRestrictedShippingIds(_paymentProviderMock.Object); + Assert.IsTrue(expectedResult.SequenceEqual(result)); + _settingService.Verify(s => s.GetSettingByKey(expectedKey, null, ""), Times.Once); + } + + [TestMethod] + public async Task GetRestrictedShippingIds_ReturnEmptyList() + { + _paymentProviderMock.Setup(c => c.SystemName).Returns("systemName"); + var expectedKey = "PaymentMethodRestictionsShipping.systemName"; + _settingService.Setup(s => s.GetSettingByKey(It.IsAny(), null, "")) + .Returns(() => Task.FromResult((PaymentRestrictedSettings)null)); + + var result = await _paymentService.GetRestrictedShippingIds(_paymentProviderMock.Object); + Assert.IsEmpty(result); + _settingService.Verify(s => s.GetSettingByKey(expectedKey, null, ""), Times.Once); + } + + [TestMethod] + public async Task GetRestrictedShippingIds_WithStoreId_PassStoreIdToSettingService() + { + _paymentProviderMock.Setup(c => c.SystemName).Returns("systemName"); + var expectedResult = new List { "Ground" }; + var expectedKey = "PaymentMethodRestictionsShipping.systemName"; + _settingService.Setup(s => s.GetSettingByKey(It.IsAny(), null, "storeId")) + .Returns(() => Task.FromResult(new PaymentRestrictedSettings { Ids = expectedResult })); + + var result = await _paymentService.GetRestrictedShippingIds(_paymentProviderMock.Object, "storeId"); + Assert.IsTrue(expectedResult.SequenceEqual(result)); + _settingService.Verify(s => s.GetSettingByKey(expectedKey, null, "storeId"), + Times.Once); + } + [TestMethod] public async Task SaveRestictedCountryIds_InvokeSettingsService() { @@ -81,6 +138,62 @@ public async Task SaveRestictedCountryIds_InvokeSettingsService() Times.Once); } + [TestMethod] + public async Task SaveRestictedCountryIds_WithStoreId_SaveSettingForStore() + { + _paymentProviderMock.Setup(c => c.SystemName).Returns("systemName"); + var countryIds = new List { "1", "2" }; + var expectedKey = "PaymentMethodRestictions.systemName"; + + await _paymentService.SaveRestrictedCountryIds(_paymentProviderMock.Object, countryIds, "storeId"); + _settingService.Verify( + s => s.SetSetting(expectedKey, + It.Is(x => x.Ids.SequenceEqual(countryIds)), "storeId"), + Times.Once); + } + + [TestMethod] + public async Task SaveRestrictedShippingIds_InvokeSettingsService() + { + _paymentProviderMock.Setup(c => c.SystemName).Returns("systemName"); + var shippingIds = new List { "Ground", "Pickup" }; + var expectedKey = "PaymentMethodRestictionsShipping.systemName"; + + await _paymentService.SaveRestrictedShippingIds(_paymentProviderMock.Object, shippingIds); + _settingService.Verify( + s => s.SetSetting(expectedKey, It.IsAny(), It.IsAny()), + Times.Once); + } + + [TestMethod] + public async Task SaveRestrictedShippingIds_WithStoreId_SaveSettingForStore() + { + _paymentProviderMock.Setup(c => c.SystemName).Returns("systemName"); + var shippingIds = new List { "Ground" }; + var expectedKey = "PaymentMethodRestictionsShipping.systemName"; + + await _paymentService.SaveRestrictedShippingIds(_paymentProviderMock.Object, shippingIds, "storeId"); + _settingService.Verify( + s => s.SetSetting(expectedKey, + It.Is(x => x.Ids.SequenceEqual(shippingIds)), "storeId"), + Times.Once); + } + + [TestMethod] + public async Task LoadAllPaymentMethods_FilterByCountry_UseStoreScopedRestrictions() + { + _paymentProviderMock.Setup(c => c.SystemName).Returns("systemName"); + var expectedKey = "PaymentMethodRestictions.systemName"; + _settingService.Setup(s => s.GetSettingByKey(expectedKey, null, "storeId")) + .Returns(() => Task.FromResult(new PaymentRestrictedSettings { Ids = ["countryId"] })); + + var result = await _paymentService.LoadAllPaymentMethods(storeId: "storeId", filterByCountryId: "countryId"); + + Assert.IsEmpty(result); + _settingService.Verify(s => s.GetSettingByKey(expectedKey, null, "storeId"), + Times.Once); + } + [TestMethod] public async Task ProcessPayment_OrderTotalZero_ReturnPaidPaymentStatus() { diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/PaymentControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/PaymentControllerTests.cs new file mode 100644 index 000000000..172c505fd --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/PaymentControllerTests.cs @@ -0,0 +1,174 @@ +using Grand.Business.Core.Interfaces.Checkout.Payments; +using Grand.Business.Core.Interfaces.Checkout.Shipping; +using Grand.Business.Core.Interfaces.Common.Configuration; +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Interfaces.Common.Stores; +using Grand.Domain.Directory; +using Grand.Domain.Shipping; +using Grand.Domain.Stores; +using Grand.Infrastructure; +using Grand.Infrastructure.Mapper; +using Grand.Mapping; +using Grand.Web.Admin.Controllers; +using Grand.Web.AdminShared.Mapper; +using Grand.Web.AdminShared.Models.Payments; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.Extensions.Primitives; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Assert = Microsoft.VisualStudio.TestTools.UnitTesting.Assert; + +namespace Grand.Web.Admin.Tests.Controllers; + +[TestClass] +public class PaymentControllerTests +{ + private const string StoreId = "storeId"; + + private PaymentController _controller; + private Mock _countryServiceMock; + private Mock _paymentServiceMock; + private Mock _settingServiceMock; + private Mock _shippingMethodServiceMock; + private Mock _translationServiceMock; + + [TestInitialize] + public void Setup() + { + var mapperConfig = new MapperConfiguration(cfg => + { + cfg.AddProfile(); + cfg.AddProfile(); + }); + AutoMapperConfig.Init(mapperConfig); + + _paymentServiceMock = new Mock(); + _settingServiceMock = new Mock(); + _countryServiceMock = new Mock(); + _shippingMethodServiceMock = new Mock(); + _translationServiceMock = new Mock(); + _translationServiceMock.Setup(t => t.GetResource(It.IsAny())).Returns("resource"); + + var contextAccessorMock = new Mock(); + contextAccessorMock.Setup(c => c.WorkContext).Returns(new Mock().Object); + + _controller = new PaymentController( + _paymentServiceMock.Object, + _settingServiceMock.Object, + _countryServiceMock.Object, + _shippingMethodServiceMock.Object, + _translationServiceMock.Object, + new Mock().Object, + contextAccessorMock.Object); + + // GetActiveStore resolves its services from HttpContext.RequestServices; + // a single store short-circuits the group/user-field lookups + var storeServiceMock = new Mock(); + storeServiceMock.Setup(s => s.GetAllStores()) + .ReturnsAsync(new List { new() { Id = StoreId } }); + var requestServicesMock = new Mock(); + requestServicesMock.Setup(sp => sp.GetService(typeof(IStoreService))).Returns(storeServiceMock.Object); + requestServicesMock.Setup(sp => sp.GetService(typeof(IContextAccessor))) + .Returns(contextAccessorMock.Object); + requestServicesMock.Setup(sp => sp.GetService(typeof(IGroupService))) + .Returns(new Mock().Object); + + var httpContext = new DefaultHttpContext { RequestServices = requestServicesMock.Object }; + httpContext.Request.Form = new FormCollection(new Dictionary()); + _controller.ControllerContext = new ControllerContext { HttpContext = httpContext }; + _controller.TempData = new TempDataDictionary(httpContext, new Mock().Object); + _controller.Url = new Mock().Object; + } + + private Mock CreatePaymentProvider(string systemName = "Payments.TestMethod") + { + var provider = new Mock(); + provider.Setup(p => p.SystemName).Returns(systemName); + provider.Setup(p => p.FriendlyName).Returns("Test method"); + return provider; + } + + [TestMethod] + public async Task MethodRestrictions_Get_BuildModelFromActiveStoreScope() + { + var provider = CreatePaymentProvider(); + var country = new Country { Id = "countryId", Name = "Poland" }; + var shippingMethod = new ShippingMethod { Name = "Ground" }; + + _paymentServiceMock.Setup(p => p.LoadAllPaymentMethods(null, "", "")) + .ReturnsAsync(new List { provider.Object }); + _countryServiceMock.Setup(c => c.GetAllCountries(It.IsAny(), It.IsAny(), true)) + .ReturnsAsync(new List { country }); + _shippingMethodServiceMock.Setup(s => s.GetAllShippingMethods(It.IsAny(), null, It.IsAny())) + .ReturnsAsync(new List { shippingMethod }); + _paymentServiceMock.Setup(p => p.GetRestrictedCountryIds(provider.Object, StoreId)) + .ReturnsAsync(new List { "countryId" }); + _paymentServiceMock.Setup(p => p.GetRestrictedShippingIds(provider.Object, StoreId)) + .ReturnsAsync(new List { "Ground" }); + + var result = await _controller.MethodRestrictions(); + + var viewResult = result as ViewResult; + Assert.IsNotNull(viewResult); + var model = viewResult.Model as PaymentMethodRestrictionModel; + Assert.IsNotNull(model); + Assert.IsTrue(model.Resticted["Payments.TestMethod"]["countryId"]); + Assert.IsTrue(model.RestictedShipping["Payments.TestMethod"]["Ground"]); + _paymentServiceMock.Verify(p => p.GetRestrictedCountryIds(provider.Object, StoreId), Times.Once); + _paymentServiceMock.Verify(p => p.GetRestrictedShippingIds(provider.Object, StoreId), Times.Once); + } + + [TestMethod] + public async Task MethodRestrictionsSave_SaveRestrictionsWithActiveStoreScope() + { + var provider = CreatePaymentProvider(); + var country = new Country { Id = "countryId", Name = "Poland" }; + var shippingMethod = new ShippingMethod { Name = "Ground" }; + + _paymentServiceMock.Setup(p => p.LoadAllPaymentMethods(null, "", "")) + .ReturnsAsync(new List { provider.Object }); + _countryServiceMock.Setup(c => c.GetAllCountries(It.IsAny(), It.IsAny(), true)) + .ReturnsAsync(new List { country }); + _shippingMethodServiceMock.Setup(s => s.GetAllShippingMethods(It.IsAny(), null, It.IsAny())) + .ReturnsAsync(new List { shippingMethod }); + + var form = new Dictionary { + ["restrict_PaymentsTestMethod"] = ["countryId"], + ["restrictship_PaymentsTestMethod"] = ["Ground"] + }; + + var result = await _controller.MethodRestrictionsSave(form); + + var redirect = result as RedirectToActionResult; + Assert.IsNotNull(redirect); + Assert.AreEqual("MethodRestrictions", redirect.ActionName); + _paymentServiceMock.Verify(p => p.SaveRestrictedCountryIds(provider.Object, + It.Is>(x => x.Single() == "countryId"), StoreId), Times.Once); + _paymentServiceMock.Verify(p => p.SaveRestrictedShippingIds(provider.Object, + It.Is>(x => x.Single() == "Ground"), StoreId), Times.Once); + } + + [TestMethod] + public async Task MethodRestrictionsSave_NoFormValues_SaveEmptyRestrictionsWithActiveStoreScope() + { + var provider = CreatePaymentProvider(); + + _paymentServiceMock.Setup(p => p.LoadAllPaymentMethods(null, "", "")) + .ReturnsAsync(new List { provider.Object }); + _countryServiceMock.Setup(c => c.GetAllCountries(It.IsAny(), It.IsAny(), true)) + .ReturnsAsync(new List()); + _shippingMethodServiceMock.Setup(s => s.GetAllShippingMethods(It.IsAny(), null, It.IsAny())) + .ReturnsAsync(new List()); + + var result = await _controller.MethodRestrictionsSave(new Dictionary()); + + Assert.IsInstanceOfType(result); + _paymentServiceMock.Verify(p => p.SaveRestrictedCountryIds(provider.Object, + It.Is>(x => x.Count == 0), StoreId), Times.Once); + _paymentServiceMock.Verify(p => p.SaveRestrictedShippingIds(provider.Object, + It.Is>(x => x.Count == 0), StoreId), Times.Once); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Services/ProductViewModelServiceTests.cs b/src/Tests/Grand.Web.Admin.Tests/Services/ProductViewModelServiceTests.cs new file mode 100644 index 000000000..956f0b515 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Services/ProductViewModelServiceTests.cs @@ -0,0 +1,206 @@ +using Grand.Business.Core.Interfaces.Catalog.Categories; +using Grand.Business.Core.Interfaces.Catalog.Collections; +using Grand.Business.Core.Interfaces.Catalog.Directory; +using Grand.Business.Core.Interfaces.Catalog.Discounts; +using Grand.Business.Core.Interfaces.Catalog.Prices; +using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Business.Core.Interfaces.Catalog.Tax; +using Grand.Business.Core.Interfaces.Checkout.Shipping; +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Interfaces.Common.Seo; +using Grand.Business.Core.Interfaces.Common.Stores; +using Grand.Business.Core.Interfaces.Customers; +using Grand.Business.Core.Interfaces.Storage; +using Grand.Domain; +using Grand.Domain.Catalog; +using Grand.Domain.Customers; +using Grand.Domain.Directory; +using Grand.Domain.Discounts; +using Grand.Domain.Media; +using Grand.Domain.Seo; +using Grand.Domain.Shipping; +using Grand.Domain.Stores; +using Grand.Domain.Tax; +using Grand.Infrastructure; +using Grand.Web.AdminShared.Models.Catalog; +using Grand.Web.AdminShared.Services; +using Grand.Web.Common.Localization; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Assert = Microsoft.VisualStudio.TestTools.UnitTesting.Assert; + +namespace Grand.Web.Admin.Tests.Services; + +[TestClass] +public class ProductViewModelServiceTests +{ + private const string StaffStoreId = "staffStoreId"; + + private Mock _discountServiceMock; + private Mock _enumTranslationServiceMock; + private Mock _measureServiceMock; + private ProductViewModelService _productViewModelService; + private Mock _storeServiceMock; + private Mock _taxCategoryServiceMock; + private Mock _translationServiceMock; + private Mock _warehouseServiceMock; + + [TestInitialize] + public void Setup() + { + _discountServiceMock = new Mock(); + _enumTranslationServiceMock = new Mock(); + _measureServiceMock = new Mock(); + _storeServiceMock = new Mock(); + _taxCategoryServiceMock = new Mock(); + _translationServiceMock = new Mock(); + _warehouseServiceMock = new Mock(); + + _translationServiceMock.Setup(t => t.GetResource(It.IsAny())).Returns("resource"); + + var workContextMock = new Mock(); + workContextMock.Setup(w => w.CurrentCustomer).Returns(new Customer { StaffStoreId = StaffStoreId }); + var contextAccessorMock = new Mock(); + contextAccessorMock.Setup(c => c.WorkContext).Returns(workContextMock.Object); + + var currencyServiceMock = new Mock(); + currencyServiceMock.Setup(c => c.GetCurrencyById(It.IsAny())).ReturnsAsync((Currency)null); + _measureServiceMock.Setup(m => m.GetMeasureWeightById(It.IsAny())).ReturnsAsync((MeasureWeight)null); + _measureServiceMock.Setup(m => m.GetMeasureDimensionById(It.IsAny())) + .ReturnsAsync((MeasureDimension)null); + _measureServiceMock.Setup(m => m.GetAllMeasureWeights()).ReturnsAsync(new List()); + _measureServiceMock.Setup(m => m.GetAllMeasureUnits()).ReturnsAsync(new List()); + + var productLayoutServiceMock = new Mock(); + productLayoutServiceMock.Setup(p => p.GetAllProductLayouts()).ReturnsAsync(new List()); + + var deliveryDateServiceMock = new Mock(); + deliveryDateServiceMock + .Setup(d => d.GetAllDeliveryDates(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new PagedList()); + + _warehouseServiceMock + .Setup(w => w.GetAllWarehouses(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new PagedList()); + + _taxCategoryServiceMock.Setup(t => t.GetAllTaxCategories(It.IsAny())) + .ReturnsAsync(new List()); + + _discountServiceMock.Setup(d => d.GetDiscountsQuery(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new List()); + + _storeServiceMock.Setup(s => s.GetAllStores()).ReturnsAsync(new List()); + + _enumTranslationServiceMock + .Setup(e => e.ToSelectList(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(new SelectList(Enumerable.Empty())); + + _productViewModelService = new ProductViewModelService( + new Mock().Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, + currencyServiceMock.Object, + _measureServiceMock.Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, + _translationServiceMock.Object, + productLayoutServiceMock.Object, + new Mock().Object, + contextAccessorMock.Object, + new Mock().Object, + _warehouseServiceMock.Object, + deliveryDateServiceMock.Object, + _taxCategoryServiceMock.Object, + _discountServiceMock.Object, + new Mock().Object, + _storeServiceMock.Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, + new CurrencySettings(), + new MeasureSettings(), + new TaxSettings(), + new SeoSettings(), + new Mock().Object, + new Mock().Object, + new Mock().Object, + _enumTranslationServiceMock.Object); + } + + [TestMethod] + public async Task PrepareProductModel_UseStaffStoreIdForTaxCategories() + { + _taxCategoryServiceMock.Setup(t => t.GetAllTaxCategories(StaffStoreId)) + .ReturnsAsync(new List { new() { Id = "taxId", Name = "Standard" } }); + + var model = new ProductModel(); + await _productViewModelService.PrepareProductModel(model, null, false, false); + + _taxCategoryServiceMock.Verify(t => t.GetAllTaxCategories(StaffStoreId), Times.Once); + Assert.IsTrue(model.AvailableTaxCategories.Any(x => x.Value == "taxId")); + } + + [TestMethod] + public async Task PrepareProductModel_UseStaffStoreIdForDiscounts() + { + var model = new ProductModel(); + await _productViewModelService.PrepareProductModel(model, null, false, false); + + _discountServiceMock.Verify(d => d.GetDiscountsQuery(DiscountType.AssignedToSkus, StaffStoreId, + It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + } + + [TestMethod] + public async Task PrepareProductModel_UseModelStoreIdForWarehouses() + { + _warehouseServiceMock + .Setup(w => w.GetAllWarehouses("modelStoreId", It.IsAny(), It.IsAny())) + .ReturnsAsync(new PagedList { new() { Id = "warehouseId", Name = "Main" } }); + + var model = new ProductModel { StoreId = "modelStoreId" }; + await _productViewModelService.PrepareProductModel(model, null, false, false); + + _warehouseServiceMock.Verify(w => w.GetAllWarehouses("modelStoreId", It.IsAny(), It.IsAny()), + Times.Once); + Assert.IsTrue(model.AvailableWarehouses.Any(x => x.Value == "warehouseId")); + } + + [TestMethod] + public async Task PrepareProductListModel_UseStoreIdForWarehouses() + { + _warehouseServiceMock + .Setup(w => w.GetAllWarehouses(StaffStoreId, It.IsAny(), It.IsAny())) + .ReturnsAsync(new PagedList { new() { Id = "warehouseId", Name = "Main" } }); + + var model = await _productViewModelService.PrepareProductListModel(StaffStoreId); + + _warehouseServiceMock.Verify(w => w.GetAllWarehouses(StaffStoreId, It.IsAny(), It.IsAny()), + Times.Once); + Assert.IsTrue(model.AvailableWarehouses.Any(x => x.Value == "warehouseId")); + } + + [TestMethod] + public async Task PrepareProductListModel_FilterStoresByStoreId() + { + _storeServiceMock.Setup(s => s.GetAllStores()).ReturnsAsync(new List { + new() { Id = "store1", Shortcut = "Store 1" }, + new() { Id = "store2", Shortcut = "Store 2" } + }); + + var model = await _productViewModelService.PrepareProductListModel("store1"); + + Assert.IsTrue(model.AvailableStores.Any(x => x.Value == "store1")); + Assert.IsFalse(model.AvailableStores.Any(x => x.Value == "store2")); + } +} diff --git a/src/Tests/Grand.Web.Store.Tests/Controllers/PaymentControllerTests.cs b/src/Tests/Grand.Web.Store.Tests/Controllers/PaymentControllerTests.cs new file mode 100644 index 000000000..57215f31e --- /dev/null +++ b/src/Tests/Grand.Web.Store.Tests/Controllers/PaymentControllerTests.cs @@ -0,0 +1,246 @@ +using Grand.Business.Core.Interfaces.Checkout.Payments; +using Grand.Business.Core.Interfaces.Checkout.Shipping; +using Grand.Business.Core.Interfaces.Common.Configuration; +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Domain.Customers; +using Grand.Domain.Directory; +using Grand.Domain.Payments; +using Grand.Domain.Shipping; +using Grand.Infrastructure; +using Grand.Infrastructure.Mapper; +using Grand.Mapping; +using Grand.Web.AdminShared.Mapper; +using Grand.Web.AdminShared.Models.Payments; +using Grand.Web.Common.DataSource; +using Grand.Web.Store.Controllers; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.Extensions.Primitives; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Store.Tests.Controllers; + +[TestClass] +public class PaymentControllerTests +{ + private const string StoreId = "storeId"; + + private PaymentController _controller; + private Mock _contextAccessorMock; + private Mock _countryServiceMock; + private Mock _paymentServiceMock; + private Mock _settingServiceMock; + private Mock _shippingMethodServiceMock; + private Mock _translationServiceMock; + + [TestInitialize] + public void Setup() + { + var mapperConfig = new MapperConfiguration(cfg => + { + cfg.AddProfile(); + cfg.AddProfile(); + cfg.AddProfile(); + }); + AutoMapperConfig.Init(mapperConfig); + + _paymentServiceMock = new Mock(); + _settingServiceMock = new Mock(); + _countryServiceMock = new Mock(); + _shippingMethodServiceMock = new Mock(); + _translationServiceMock = new Mock(); + _translationServiceMock.Setup(t => t.GetResource(It.IsAny())).Returns("resource"); + + var workContextMock = new Mock(); + workContextMock.Setup(w => w.CurrentCustomer).Returns(new Customer { StaffStoreId = StoreId }); + _contextAccessorMock = new Mock(); + _contextAccessorMock.Setup(c => c.WorkContext).Returns(workContextMock.Object); + + _controller = new PaymentController( + _paymentServiceMock.Object, + _settingServiceMock.Object, + _countryServiceMock.Object, + _shippingMethodServiceMock.Object, + _translationServiceMock.Object, + _contextAccessorMock.Object); + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Form = new FormCollection(new Dictionary()); + _controller.ControllerContext = new ControllerContext { HttpContext = httpContext }; + _controller.TempData = new TempDataDictionary(httpContext, new Mock().Object); + } + + private Mock CreatePaymentProvider(string systemName = "Payments.TestMethod") + { + var provider = new Mock(); + provider.Setup(p => p.SystemName).Returns(systemName); + provider.Setup(p => p.FriendlyName).Returns("Test method"); + return provider; + } + + [TestMethod] + public async Task Methods_ReturnPaymentMethodsForCurrentStore() + { + var provider = CreatePaymentProvider(); + _paymentServiceMock.Setup(p => p.LoadAllPaymentMethods(null, StoreId, "")) + .ReturnsAsync(new List { provider.Object }); + _settingServiceMock.Setup(s => s.LoadSetting(StoreId)) + .ReturnsAsync(new PaymentSettings { + ActivePaymentProviderSystemNames = ["Payments.TestMethod"] + }); + + var result = await _controller.Methods(); + + var jsonResult = result as JsonResult; + Assert.IsNotNull(jsonResult); + var data = (DataSourceResult)jsonResult.Value; + Assert.AreEqual(1, data.Total); + var model = ((IEnumerable)data.Data).First(); + Assert.AreEqual("Payments.TestMethod", model.SystemName); + Assert.IsTrue(model.IsActive); + _paymentServiceMock.Verify(p => p.LoadAllPaymentMethods(null, StoreId, ""), Times.Once); + _settingServiceMock.Verify(s => s.LoadSetting(StoreId), Times.Once); + } + + [TestMethod] + public async Task MethodUpdate_ActivateMethod_SaveSettingForCurrentStore() + { + var provider = CreatePaymentProvider(); + var paymentSettings = new PaymentSettings(); + _paymentServiceMock.Setup(p => p.LoadPaymentMethodBySystemName("Payments.TestMethod")) + .Returns(provider.Object); + _settingServiceMock.Setup(s => s.LoadSetting(StoreId)).ReturnsAsync(paymentSettings); + + var result = await _controller.MethodUpdate(new PaymentMethodModel + { SystemName = "Payments.TestMethod", IsActive = true }); + + Assert.IsInstanceOfType(result); + Assert.Contains("Payments.TestMethod", paymentSettings.ActivePaymentProviderSystemNames); + _settingServiceMock.Verify(s => s.SaveSetting(paymentSettings, StoreId), Times.Once); + } + + [TestMethod] + public async Task MethodUpdate_DeactivateMethod_SaveSettingForCurrentStore() + { + var provider = CreatePaymentProvider(); + var paymentSettings = new PaymentSettings { + ActivePaymentProviderSystemNames = ["Payments.TestMethod"] + }; + _paymentServiceMock.Setup(p => p.LoadPaymentMethodBySystemName("Payments.TestMethod")) + .Returns(provider.Object); + _settingServiceMock.Setup(s => s.LoadSetting(StoreId)).ReturnsAsync(paymentSettings); + + var result = await _controller.MethodUpdate(new PaymentMethodModel + { SystemName = "Payments.TestMethod", IsActive = false }); + + Assert.IsInstanceOfType(result); + Assert.DoesNotContain("Payments.TestMethod", paymentSettings.ActivePaymentProviderSystemNames); + _settingServiceMock.Verify(s => s.SaveSetting(paymentSettings, StoreId), Times.Once); + } + + [TestMethod] + public async Task MethodUpdate_UnknownMethod_NotSaveSetting() + { + _paymentServiceMock.Setup(p => p.LoadPaymentMethodBySystemName(It.IsAny())) + .Returns((IPaymentProvider)null); + _settingServiceMock.Setup(s => s.LoadSetting(StoreId)).ReturnsAsync(new PaymentSettings()); + + var result = await _controller.MethodUpdate(new PaymentMethodModel + { SystemName = "Payments.Unknown", IsActive = true }); + + Assert.IsInstanceOfType(result); + _settingServiceMock.Verify(s => s.SaveSetting(It.IsAny(), It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task Settings_Get_LoadSettingForCurrentStore() + { + _settingServiceMock.Setup(s => s.LoadSetting(StoreId)) + .ReturnsAsync(new PaymentSettings { AllowRePostingPayments = true }); + + var result = await _controller.Settings(); + + var viewResult = result as ViewResult; + Assert.IsNotNull(viewResult); + var model = viewResult.Model as PaymentSettingsModel; + Assert.IsNotNull(model); + Assert.IsTrue(model.AllowRePostingPayments); + _settingServiceMock.Verify(s => s.LoadSetting(StoreId), Times.Once); + } + + [TestMethod] + public async Task Settings_Post_SaveSettingForCurrentStoreAndRedirect() + { + var paymentSettings = new PaymentSettings(); + _settingServiceMock.Setup(s => s.LoadSetting(StoreId)).ReturnsAsync(paymentSettings); + + var result = await _controller.Settings(new PaymentSettingsModel { AllowRePostingPayments = true }); + + var redirect = result as RedirectToActionResult; + Assert.IsNotNull(redirect); + Assert.AreEqual("Settings", redirect.ActionName); + _settingServiceMock.Verify(s => s.SaveSetting(It.IsAny(), StoreId), Times.Once); + } + + [TestMethod] + public async Task MethodRestrictions_Get_BuildModelFromStoreScopedRestrictions() + { + var provider = CreatePaymentProvider(); + var country = new Country { Id = "countryId", Name = "Poland" }; + var shippingMethod = new ShippingMethod { Name = "Ground" }; + + _paymentServiceMock.Setup(p => p.LoadAllPaymentMethods(null, StoreId, "")) + .ReturnsAsync(new List { provider.Object }); + _countryServiceMock.Setup(c => c.GetAllCountries(It.IsAny(), It.IsAny(), true)) + .ReturnsAsync(new List { country }); + _shippingMethodServiceMock.Setup(s => s.GetAllShippingMethods(It.IsAny(), null, StoreId)) + .ReturnsAsync(new List { shippingMethod }); + _paymentServiceMock.Setup(p => p.GetRestrictedCountryIds(provider.Object, StoreId)) + .ReturnsAsync(new List { "countryId" }); + _paymentServiceMock.Setup(p => p.GetRestrictedShippingIds(provider.Object, StoreId)) + .ReturnsAsync(new List { "Ground" }); + + var result = await _controller.MethodRestrictions(); + + var viewResult = result as ViewResult; + Assert.IsNotNull(viewResult); + var model = viewResult.Model as PaymentMethodRestrictionModel; + Assert.IsNotNull(model); + Assert.IsTrue(model.Resticted["Payments.TestMethod"]["countryId"]); + Assert.IsTrue(model.RestictedShipping["Payments.TestMethod"]["Ground"]); + _paymentServiceMock.Verify(p => p.GetRestrictedCountryIds(provider.Object, StoreId), Times.Once); + _paymentServiceMock.Verify(p => p.GetRestrictedShippingIds(provider.Object, StoreId), Times.Once); + } + + [TestMethod] + public async Task MethodRestrictionsSave_SaveRestrictionsForCurrentStore() + { + var provider = CreatePaymentProvider(); + var country = new Country { Id = "countryId", Name = "Poland" }; + var shippingMethod = new ShippingMethod { Name = "Ground" }; + + _paymentServiceMock.Setup(p => p.LoadAllPaymentMethods(null, StoreId, "")) + .ReturnsAsync(new List { provider.Object }); + _countryServiceMock.Setup(c => c.GetAllCountries(It.IsAny(), It.IsAny(), true)) + .ReturnsAsync(new List { country }); + _shippingMethodServiceMock.Setup(s => s.GetAllShippingMethods(It.IsAny(), null, StoreId)) + .ReturnsAsync(new List { shippingMethod }); + + var form = new Dictionary { + ["restrict_PaymentsTestMethod"] = ["countryId"] + }; + + var result = await _controller.MethodRestrictionsSave(form); + + var redirect = result as RedirectToActionResult; + Assert.IsNotNull(redirect); + Assert.AreEqual("MethodRestrictions", redirect.ActionName); + _paymentServiceMock.Verify(p => p.SaveRestrictedCountryIds(provider.Object, + It.Is>(x => x.Single() == "countryId"), StoreId), Times.Once); + _paymentServiceMock.Verify(p => p.SaveRestrictedShippingIds(provider.Object, + It.Is>(x => x.Count == 0), StoreId), Times.Once); + } +} diff --git a/src/Tests/Grand.Web.Store.Tests/Controllers/ShippingControllerTests.cs b/src/Tests/Grand.Web.Store.Tests/Controllers/ShippingControllerTests.cs new file mode 100644 index 000000000..a49a2cde4 --- /dev/null +++ b/src/Tests/Grand.Web.Store.Tests/Controllers/ShippingControllerTests.cs @@ -0,0 +1,153 @@ +using Grand.Business.Core.Interfaces.Checkout.Shipping; +using Grand.Business.Core.Interfaces.Common.Configuration; +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Domain; +using Grand.Domain.Customers; +using Grand.Domain.Directory; +using Grand.Domain.Shipping; +using Grand.Infrastructure; +using Grand.Web.AdminShared.Models.Shipping; +using Grand.Web.Store.Controllers; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.Extensions.Primitives; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Store.Tests.Controllers; + +[TestClass] +public class ShippingControllerTests +{ + private const string StoreId = "storeId"; + + private ShippingController _controller; + private Mock _contextAccessorMock; + private Mock _countryServiceMock; + private Mock _groupServiceMock; + private Mock _shippingMethodServiceMock; + private Mock _translationServiceMock; + + [TestInitialize] + public void Setup() + { + _shippingMethodServiceMock = new Mock(); + _countryServiceMock = new Mock(); + _groupServiceMock = new Mock(); + _translationServiceMock = new Mock(); + _translationServiceMock.Setup(t => t.GetResource(It.IsAny())).Returns("resource"); + + var workContextMock = new Mock(); + workContextMock.Setup(w => w.CurrentCustomer).Returns(new Customer { StaffStoreId = StoreId }); + _contextAccessorMock = new Mock(); + _contextAccessorMock.Setup(c => c.WorkContext).Returns(workContextMock.Object); + + _controller = new ShippingController( + new Mock().Object, + _shippingMethodServiceMock.Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, + _countryServiceMock.Object, + _groupServiceMock.Object, + new Mock().Object, + _translationServiceMock.Object, + new Mock().Object, + _contextAccessorMock.Object); + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Form = new FormCollection(new Dictionary()); + _controller.ControllerContext = new ControllerContext { HttpContext = httpContext }; + _controller.TempData = new TempDataDictionary(httpContext, new Mock().Object); + } + + private void SetupCommonData(Country country, ShippingMethod shippingMethod, CustomerGroup customerGroup) + { + _countryServiceMock.Setup(c => c.GetAllCountries(It.IsAny(), It.IsAny(), true)) + .ReturnsAsync(new List { country }); + _shippingMethodServiceMock.Setup(s => s.GetAllShippingMethods(It.IsAny(), null, StoreId)) + .ReturnsAsync(new List { shippingMethod }); + _groupServiceMock.Setup(g => + g.GetAllCustomerGroups(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new PagedList { customerGroup }); + } + + [TestMethod] + public async Task Restrictions_Get_BuildModelFromStoreScopedShippingMethods() + { + var country = new Country { Id = "countryId", Name = "Poland" }; + var customerGroup = new CustomerGroup { Name = "Guests" }; + var shippingMethod = new ShippingMethod { Name = "Ground" }; + shippingMethod.RestrictedCountries.Add(country); + shippingMethod.RestrictedGroups.Add(customerGroup.Id); + SetupCommonData(country, shippingMethod, customerGroup); + + var result = await _controller.Restrictions(); + + var viewResult = result as ViewResult; + Assert.IsNotNull(viewResult); + var model = viewResult.Model as ShippingMethodRestrictionModel; + Assert.IsNotNull(model); + Assert.IsTrue(model.Restricted["countryId"][shippingMethod.Id]); + Assert.IsTrue(model.RestictedGroup[customerGroup.Id][shippingMethod.Id]); + _shippingMethodServiceMock.Verify(s => s.GetAllShippingMethods(It.IsAny(), null, StoreId), + Times.Once); + } + + [TestMethod] + public async Task RestrictionSave_AddRestriction_UpdateShippingMethod() + { + var country = new Country { Id = "countryId", Name = "Poland" }; + var customerGroup = new CustomerGroup { Name = "Guests" }; + var shippingMethod = new ShippingMethod { Name = "Ground" }; + SetupCommonData(country, shippingMethod, customerGroup); + + var form = new Dictionary { + [$"restrict_{shippingMethod.Id}"] = ["countryId"] + }; + + var result = await _controller.RestrictionSave(form); + + var redirect = result as RedirectToActionResult; + Assert.IsNotNull(redirect); + Assert.AreEqual("Restrictions", redirect.ActionName); + Assert.IsTrue(shippingMethod.RestrictedCountries.Any(c => c.Id == "countryId")); + _shippingMethodServiceMock.Verify(s => s.UpdateShippingMethod(shippingMethod), Times.Once); + _shippingMethodServiceMock.Verify(s => s.GetAllShippingMethods(It.IsAny(), null, StoreId), + Times.Once); + } + + [TestMethod] + public async Task RestrictionSave_NoFormValues_ClearExistingRestrictions() + { + var country = new Country { Id = "countryId", Name = "Poland" }; + var customerGroup = new CustomerGroup { Name = "Guests" }; + var shippingMethod = new ShippingMethod { Name = "Ground" }; + shippingMethod.RestrictedCountries.Add(country); + shippingMethod.RestrictedGroups.Add(customerGroup.Id); + SetupCommonData(country, shippingMethod, customerGroup); + + var result = await _controller.RestrictionSave(new Dictionary()); + + Assert.IsInstanceOfType(result); + Assert.IsEmpty(shippingMethod.RestrictedCountries); + Assert.IsEmpty(shippingMethod.RestrictedGroups); + _shippingMethodServiceMock.Verify(s => s.UpdateShippingMethod(shippingMethod), Times.Exactly(2)); + } + + [TestMethod] + public async Task RestrictionSave_NoChanges_NotUpdateShippingMethod() + { + var country = new Country { Id = "countryId", Name = "Poland" }; + var customerGroup = new CustomerGroup { Name = "Guests" }; + var shippingMethod = new ShippingMethod { Name = "Ground" }; + SetupCommonData(country, shippingMethod, customerGroup); + + var result = await _controller.RestrictionSave(new Dictionary()); + + Assert.IsInstanceOfType(result); + _shippingMethodServiceMock.Verify(s => s.UpdateShippingMethod(It.IsAny()), Times.Never); + } +} diff --git a/src/Tests/Grand.Web.Store.Tests/Grand.Web.Store.Tests.csproj b/src/Tests/Grand.Web.Store.Tests/Grand.Web.Store.Tests.csproj new file mode 100644 index 000000000..27b6e3c8d --- /dev/null +++ b/src/Tests/Grand.Web.Store.Tests/Grand.Web.Store.Tests.csproj @@ -0,0 +1,24 @@ + + + + + false + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + From aca3deb8d2fd6be32523b73bd84900e9a660f7df Mon Sep 17 00:00:00 2001 From: KrzysztofPajak Date: Fri, 12 Jun 2026 20:44:30 +0200 Subject: [PATCH 5/5] Fix pipelines --- .github/workflows/aspnetcore.yml | 4 ++++ .github/workflows/build.yml | 27 +++++++++++++++++---------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/.github/workflows/aspnetcore.yml b/.github/workflows/aspnetcore.yml index 6e5b25712..f9fcb6cf7 100644 --- a/.github/workflows/aspnetcore.yml +++ b/.github/workflows/aspnetcore.yml @@ -49,7 +49,11 @@ jobs: run: dotnet test ./src/Tests/Grand.Infrastructure.Tests/Grand.Infrastructure.Tests.csproj - name: Grand.SharedKernel.Tests Unit Tests run: dotnet test ./src/Tests/Grand.SharedKernel.Tests/Grand.SharedKernel.Tests.csproj + - name: Grand.Mapping.Tests Unit Tests + run: dotnet test ./src/Tests/Grand.Mapping.Tests/Grand.Mapping.Tests.csproj - name: Grand.Web.Common.Tests Unit Tests run: dotnet test ./src/Tests/Grand.Web.Common.Tests/Grand.Web.Common.Tests.csproj - name: Grand.Web.Admin.Tests Unit Tests run: dotnet test ./src/Tests/Grand.Web.Admin.Tests/Grand.Web.Admin.Tests.csproj + - name: Grand.Web.Store.Tests Unit Tests + run: dotnet test ./src/Tests/Grand.Web.Store.Tests/Grand.Web.Store.Tests.csproj diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0b647f0a3..054110238 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,7 +8,7 @@ on: jobs: build: name: Build and analyze - runs-on: windows-latest + runs-on: ubuntu-latest steps: - name: Set up JDK 17 uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4.8.0 @@ -18,30 +18,37 @@ jobs: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + - name: Setup .NET Core + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + - name: Install .NET Aspire workload + run: dotnet workload install aspire - name: Cache SonarQube Cloud packages uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: - path: ~\sonar\cache + path: ~/.sonar/cache key: ${{ runner.os }}-sonar restore-keys: ${{ runner.os }}-sonar - name: Cache SonarQube Cloud scanner id: cache-sonar-scanner uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: - path: ${{ runner.temp }}\scanner + path: ./.sonar/scanner key: ${{ runner.os }}-sonar-scanner restore-keys: ${{ runner.os }}-sonar-scanner - name: Install SonarQube Cloud scanner if: steps.cache-sonar-scanner.outputs.cache-hit != 'true' - shell: powershell run: | - New-Item -Path ${{ runner.temp }}\scanner -ItemType Directory - dotnet tool update dotnet-sonarscanner --tool-path ${{ runner.temp }}\scanner + mkdir -p ./.sonar/scanner + dotnet tool update dotnet-sonarscanner --tool-path ./.sonar/scanner + - name: Start Docker for Mongodb + run: docker run -d -p 27017:27017 mongo - name: Build and analyze env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - shell: powershell run: | - ${{ runner.temp }}\scanner\dotnet-sonarscanner begin /k:"grandnode_grandnode2" /o:"grandnode" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" - dotnet build - ${{ runner.temp }}\scanner\dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}" + ./.sonar/scanner/dotnet-sonarscanner begin /k:"grandnode_grandnode2" /o:"grandnode" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" + dotnet build ./GrandNode.sln + dotnet test ./GrandNode.sln --no-build --collect:"XPlat Code Coverage" -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover + ./.sonar/scanner/dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"