diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseVendorReviewControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseVendorReviewControllerTests.cs new file mode 100644 index 000000000..d88f6ccac --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseVendorReviewControllerTests.cs @@ -0,0 +1,193 @@ +using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Interfaces.Customers; +using Grand.Domain.Vendors; +using Grand.Web.AdminShared.Controllers; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Vendors; +using Grand.Web.Common.DataSource; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Admin.Tests.Controllers; + +[TestClass] +public class BaseVendorReviewControllerTests +{ + // BaseVendorReviewController is abstract; minimal subclass so actions can be invoked directly. + private class TestVendorReviewController( + IVendorViewModelService vendorViewModelService, + IVendorService vendorService, + ITranslationService translationService, + IAdminDataScope scope) + : BaseVendorReviewController(vendorViewModelService, vendorService, translationService, scope); + + private TestVendorReviewController _controller; + private Mock _vendorViewModelServiceMock; + private Mock _vendorServiceMock; + private Mock> _scopeMock; + + [TestInitialize] + public void Setup() + { + _vendorViewModelServiceMock = new Mock(); + _vendorServiceMock = new Mock(); + _scopeMock = new Mock>(); + _scopeMock.Setup(s => s.DefaultVendorId).Returns((string)null); + + var translationServiceMock = new Mock(); + translationServiceMock.Setup(t => t.GetResource(It.IsAny())).Returns("resource"); + + _controller = new TestVendorReviewController( + _vendorViewModelServiceMock.Object, + _vendorServiceMock.Object, + translationServiceMock.Object, + _scopeMock.Object); + + var httpContext = new DefaultHttpContext(); + var loggerFactoryMock = new Mock(); + loggerFactoryMock.Setup(l => l.CreateLogger(It.IsAny())).Returns(new Mock().Object); + var urlHelperFactoryMock = new Mock(); + urlHelperFactoryMock.Setup(f => f.GetUrlHelper(It.IsAny())).Returns(new Mock().Object); + var requestServicesMock = new Mock(); + requestServicesMock.Setup(s => s.GetService(typeof(ILoggerFactory))).Returns(loggerFactoryMock.Object); + requestServicesMock.Setup(s => s.GetService(typeof(IUrlHelperFactory))).Returns(urlHelperFactoryMock.Object); + httpContext.RequestServices = requestServicesMock.Object; + _controller.ControllerContext = new ControllerContext { HttpContext = httpContext }; + _controller.TempData = new TempDataDictionary(httpContext, new Mock().Object); + } + + [TestMethod] + public void Index_RedirectsToList() + { + var result = _controller.Index() as RedirectToActionResult; + Assert.IsNotNull(result); + Assert.AreEqual("List", result.ActionName); + } + + [TestMethod] + public void List_Get_ReturnsViewWithEmptyModel() + { + var result = _controller.List() as ViewResult; + Assert.IsNotNull(result); + Assert.IsInstanceOfType(result.Model, typeof(VendorReviewListModel)); + } + + [TestMethod] + public async Task List_Post_GlobalScope_DoesNotForceSearchVendorId() + { + var model = new VendorReviewListModel { SearchVendorId = "caller-picked" }; + _vendorViewModelServiceMock + .Setup(v => v.PrepareVendorReviewModel(model, 1, 10)) + .ReturnsAsync((new List(), 0)); + + await _controller.List(new DataSourceRequest { Page = 1, PageSize = 10 }, model); + + Assert.AreEqual("caller-picked", model.SearchVendorId); + _vendorViewModelServiceMock.Verify(v => v.PrepareVendorReviewModel(model, 1, 10), Times.Once); + } + + [TestMethod] + public async Task List_Post_VendorScope_ForcesSearchVendorId() + { + _scopeMock.Setup(s => s.DefaultVendorId).Returns("vendor-A"); + var model = new VendorReviewListModel { SearchVendorId = "caller-picked" }; + _vendorViewModelServiceMock + .Setup(v => v.PrepareVendorReviewModel(model, 1, 10)) + .ReturnsAsync((new List(), 0)); + + await _controller.List(new DataSourceRequest { Page = 1, PageSize = 10 }, model); + + Assert.AreEqual("vendor-A", model.SearchVendorId); + } + + [TestMethod] + public async Task Edit_Get_NotFound_RedirectsToList() + { + _vendorServiceMock.Setup(v => v.GetVendorReviewById("missing")).ReturnsAsync((VendorReview)null); + + var result = await _controller.Edit("missing") as RedirectToActionResult; + + Assert.IsNotNull(result); + Assert.AreEqual("List", result.ActionName); + } + + [TestMethod] + public async Task Edit_Get_AccessDenied_RedirectsToList() + { + var review = new VendorReview { Id = "r1", VendorId = "vendor-OTHER" }; + _vendorServiceMock.Setup(v => v.GetVendorReviewById("r1")).ReturnsAsync(review); + _scopeMock.Setup(s => s.HasAccess(review)).ReturnsAsync(false); + + var result = await _controller.Edit("r1") as RedirectToActionResult; + + Assert.IsNotNull(result); + Assert.AreEqual("List", result.ActionName); + } + + [TestMethod] + public async Task Edit_Get_AccessGranted_ReturnsView() + { + var review = new VendorReview { Id = "r1", VendorId = "vendor-A" }; + _vendorServiceMock.Setup(v => v.GetVendorReviewById("r1")).ReturnsAsync(review); + _scopeMock.Setup(s => s.HasAccess(review)).ReturnsAsync(true); + + var result = await _controller.Edit("r1") as ViewResult; + + Assert.IsNotNull(result); + _vendorViewModelServiceMock.Verify( + v => v.PrepareVendorReviewModel(It.IsAny(), review, false, false), Times.Once); + } + + [TestMethod] + public async Task Delete_AccessDenied_RedirectsToList() + { + var review = new VendorReview { Id = "r1", VendorId = "vendor-OTHER" }; + _vendorServiceMock.Setup(v => v.GetVendorReviewById("r1")).ReturnsAsync(review); + _scopeMock.Setup(s => s.HasAccess(review)).ReturnsAsync(false); + + var result = await _controller.Delete("r1") as RedirectToActionResult; + + Assert.IsNotNull(result); + Assert.AreEqual("List", result.ActionName); + _vendorViewModelServiceMock.Verify(v => v.DeleteVendorReview(It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task Delete_AccessGranted_DeletesAndRedirects() + { + var review = new VendorReview { Id = "r1", VendorId = "vendor-A" }; + _vendorServiceMock.Setup(v => v.GetVendorReviewById("r1")).ReturnsAsync(review); + _scopeMock.Setup(s => s.HasAccess(review)).ReturnsAsync(true); + + var result = await _controller.Delete("r1") as RedirectToActionResult; + + Assert.IsNotNull(result); + Assert.AreEqual("List", result.ActionName); + _vendorViewModelServiceMock.Verify(v => v.DeleteVendorReview(review), Times.Once); + } + + [TestMethod] + public async Task ApproveSelected_ForwardsSelectedIdsAndScope() + { + var ids = new List { "r1:vendor-A" }; + + await _controller.ApproveSelected(ids); + + _vendorViewModelServiceMock.Verify(v => v.ApproveVendorReviews(ids, _scopeMock.Object), Times.Once); + } + + [TestMethod] + public async Task DisapproveSelected_ForwardsSelectedIdsAndScope() + { + var ids = new List { "r1:vendor-A" }; + + await _controller.DisapproveSelected(ids); + + _vendorViewModelServiceMock.Verify(v => v.DisapproveVendorReviews(ids, _scopeMock.Object), Times.Once); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/RoutedVendorReviewDataScopeTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/RoutedVendorReviewDataScopeTests.cs new file mode 100644 index 000000000..12bc12d42 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/RoutedVendorReviewDataScopeTests.cs @@ -0,0 +1,71 @@ +#nullable enable + +using Grand.Domain.Vendors; +using Grand.Infrastructure; +using Grand.Web.AdminShared.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Admin.Tests.Controllers; + +[TestClass] +public class RoutedVendorReviewDataScopeTests +{ + private const string VendorId = "vendor-1"; + + private GlobalAdminDataScope _adminScope = null!; + private VendorVendorReviewDataScope _vendorScope = null!; + + [TestInitialize] + public void Setup() + { + var workContext = new Mock(); + workContext.Setup(x => x.CurrentVendor).Returns(new Vendor { Id = VendorId }); + var contextAccessor = new Mock(); + contextAccessor.Setup(x => x.WorkContext).Returns(workContext.Object); + + _adminScope = new GlobalAdminDataScope(); + _vendorScope = new VendorVendorReviewDataScope(contextAccessor.Object); + } + + private RoutedVendorReviewDataScope ResolverForArea(string? area) + { + var httpContext = new DefaultHttpContext(); + if (area is not null) httpContext.Request.RouteValues["area"] = area; + var httpContextAccessor = new Mock(); + httpContextAccessor.Setup(x => x.HttpContext).Returns(httpContext); + return new RoutedVendorReviewDataScope(httpContextAccessor.Object, _adminScope, _vendorScope); + } + + [TestMethod] + public void AdminArea_ResolvesToAdminScope() + { + var resolver = ResolverForArea("Admin"); + Assert.IsNull(resolver.DefaultStoreId); + Assert.IsNull(resolver.DefaultVendorId); + Assert.AreEqual("Admin", resolver.ResourceKeyPrefix); + } + + [TestMethod] + public void VendorArea_ResolvesToVendorScope() + { + var resolver = ResolverForArea("Vendor"); + Assert.AreEqual("Vendor", resolver.ResourceKeyPrefix); + Assert.AreEqual(VendorId, resolver.DefaultVendorId); + Assert.IsFalse(resolver.ShowStoreSelector); + } + + [TestMethod] + public void UnrecognizedOrMissingArea_ThrowsFailClosed() + { + var resolverStore = ResolverForArea("Store"); + Assert.Throws(() => _ = resolverStore.ResourceKeyPrefix); + + var resolverTypo = ResolverForArea("Vendorr"); + Assert.Throws(() => _ = resolverTypo.ResourceKeyPrefix); + + var resolverNoArea = ResolverForArea(null); + Assert.Throws(() => _ = resolverNoArea.ResourceKeyPrefix); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/VendorReviewControllerRoutingTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/VendorReviewControllerRoutingTests.cs new file mode 100644 index 000000000..f80cf1cc4 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/VendorReviewControllerRoutingTests.cs @@ -0,0 +1,49 @@ +using Grand.Web.Admin.Controllers; +using Grand.Web.Admin.Extensions; +using Grand.Web.AdminShared.Controllers; +using Grand.Web.Common.Filters; +using Microsoft.AspNetCore.Mvc; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Grand.Web.Admin.Tests.Controllers; + +[TestClass] +public class VendorReviewControllerRoutingTests +{ + [TestMethod] + public void AdminVendorReviewController_InheritsBaseVendorReviewController() => + Assert.IsTrue(typeof(BaseVendorReviewController).IsAssignableFrom(typeof(VendorReviewController))); + + [TestMethod] + public void AdminVendorReviewController_HasAutoValidateAntiforgeryToken() => + Assert.IsTrue(typeof(VendorReviewController) + .GetCustomAttributes(typeof(Microsoft.AspNetCore.Mvc.AutoValidateAntiforgeryTokenAttribute), false) + .Length > 0); + + [TestMethod] + public void AdminVendorReviewController_HasAreaAttributeWithAdminArea() + { + var areaAttr = (AreaAttribute)Attribute.GetCustomAttribute(typeof(VendorReviewController), typeof(AreaAttribute), false); + Assert.IsNotNull(areaAttr, "Missing [Area]."); + Assert.AreEqual(Constants.AreaAdmin, areaAttr.RouteValue); + } + + [TestMethod] + public void AdminVendorReviewController_HasAuthorizeAdminAttribute() => + Assert.IsTrue(typeof(VendorReviewController).IsDefined(typeof(AuthorizeAdminAttribute), false), + "Missing [AuthorizeAdmin]."); + + // Regression guard: VendorSearchAutoComplete leaks other vendors' names/ids and must remain + // Admin-only - never silently dropped from Admin, never promoted to the shared base controller. + [TestMethod] + public void AdminVendorReviewController_DeclaresVendorSearchAutoComplete() + { + var declaredMethodNames = typeof(VendorReviewController) + .GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance) + .Select(m => m.Name) + .ToHashSet(); + + Assert.IsTrue(declaredMethodNames.Contains("VendorSearchAutoComplete"), + "Admin's VendorReviewController is missing VendorSearchAutoComplete."); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/VendorVendorReviewDataScopeTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/VendorVendorReviewDataScopeTests.cs new file mode 100644 index 000000000..4a1618729 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/VendorVendorReviewDataScopeTests.cs @@ -0,0 +1,66 @@ +using Grand.Domain.Vendors; +using Grand.Infrastructure; +using Grand.Web.AdminShared.Services; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Admin.Tests.Controllers; + +[TestClass] +public class VendorVendorReviewDataScopeTests +{ + private static VendorVendorReviewDataScope Build(string currentVendorId) + { + var workContextMock = new Mock(); + workContextMock.Setup(w => w.CurrentVendor).Returns(new Vendor { Id = currentVendorId }); + var contextAccessorMock = new Mock(); + contextAccessorMock.Setup(c => c.WorkContext).Returns(workContextMock.Object); + return new VendorVendorReviewDataScope(contextAccessorMock.Object); + } + + [TestMethod] + public async Task HasAccess_MatchingVendorId_True() + { + var scope = Build("vendor-A"); + Assert.IsTrue(await scope.HasAccess(new VendorReview { VendorId = "vendor-A" })); + } + + [TestMethod] + public async Task HasAccess_MismatchedVendorId_False() + { + var scope = Build("vendor-A"); + Assert.IsFalse(await scope.HasAccess(new VendorReview { VendorId = "vendor-B" })); + } + + [TestMethod] + public async Task HasAccess_NullEntity_False() + { + var scope = Build("vendor-A"); + Assert.IsFalse(await scope.HasAccess(null)); + } + + [TestMethod] + public async Task HasAccess_EmptyCurrentVendorIdAndEmptyEntityVendorId_False() + { + var scope = Build(string.Empty); + Assert.IsFalse(await scope.HasAccess(new VendorReview { VendorId = string.Empty })); + } + + [TestMethod] + public async Task HasAccess_NullCurrentVendorIdAndNullEntityVendorId_False() + { + var scope = Build(null); + Assert.IsFalse(await scope.HasAccess(new VendorReview { VendorId = null })); + } + + [TestMethod] + public void ScopeDefaults_VendorScoped() + { + var scope = Build("vendor-A"); + Assert.IsNull(scope.DefaultStoreId); + Assert.AreEqual("vendor-A", scope.DefaultVendorId); + Assert.AreEqual("Vendor", scope.ResourceKeyPrefix); + Assert.IsFalse(scope.ShowStoreSelector); + Assert.IsFalse(scope.CanFeatureOnHomepage); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Services/VendorViewModelServiceTests.cs b/src/Tests/Grand.Web.Admin.Tests/Services/VendorViewModelServiceTests.cs new file mode 100644 index 000000000..02d634d19 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Services/VendorViewModelServiceTests.cs @@ -0,0 +1,167 @@ +using Grand.Business.Core.Interfaces.Catalog.Discounts; +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.Customers; +using Grand.Domain.Vendors; +using Grand.Infrastructure; +using Grand.Mediator; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Vendors; +using Grand.Web.AdminShared.Services; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Assert = Microsoft.VisualStudio.TestTools.UnitTesting.Assert; + +namespace Grand.Web.Admin.Tests.Services; + +[TestClass] +public class VendorViewModelServiceTests +{ + private Mock _discountServiceMock; + private Mock _vendorServiceMock; + private Mock _customerServiceMock; + private Mock _translationServiceMock; + private Mock _dateTimeServiceMock; + private Mock _countryServiceMock; + private Mock _storeServiceMock; + private Mock _pictureServiceMock; + private Mock _mediatorMock; + private VendorViewModelService _service; + + [TestInitialize] + public void Setup() + { + _discountServiceMock = new Mock(); + _vendorServiceMock = new Mock(); + _customerServiceMock = new Mock(); + _translationServiceMock = new Mock(); + _dateTimeServiceMock = new Mock(); + _countryServiceMock = new Mock(); + _storeServiceMock = new Mock(); + _pictureServiceMock = new Mock(); + _mediatorMock = new Mock(); + + _service = new VendorViewModelService( + _discountServiceMock.Object, + _vendorServiceMock.Object, + _customerServiceMock.Object, + _translationServiceMock.Object, + _dateTimeServiceMock.Object, + _countryServiceMock.Object, + _storeServiceMock.Object, + _pictureServiceMock.Object, + _mediatorMock.Object, + new VendorSettings(), + new Mock().Object); + } + + [TestMethod] + public async Task PrepareVendorReviewModel_Entity_AlwaysPopulatesVendorIdAndName() + { + var vendorReview = new VendorReview { Id = "review-1", VendorId = "vendor-1", CustomerId = "cust-1" }; + _vendorServiceMock.Setup(v => v.GetVendorById("vendor-1")) + .ReturnsAsync(new Vendor { Id = "vendor-1", Name = "Acme Vendor" }); + _customerServiceMock.Setup(c => c.GetCustomerById("cust-1")) + .ReturnsAsync(new Customer { Id = "cust-1", Email = "buyer@example.com" }); + + var model = new VendorReviewModel(); + await _service.PrepareVendorReviewModel(model, vendorReview, false, false); + + Assert.AreEqual("vendor-1", model.VendorId); + Assert.AreEqual("Acme Vendor", model.VendorName); + Assert.AreEqual("review-1:vendor-1", model.Ids); + } + + [TestMethod] + public async Task PrepareVendorReviewModel_List_GlobalScope_UsesModelSearchVendorId() + { + _vendorServiceMock.Setup(v => v.GetAllVendorReviews("", null, null, null, null, "vendor-picked", 0, 10)) + .ReturnsAsync(new PagedList(new List(), 0, 10)); + + var listModel = new VendorReviewListModel { SearchVendorId = "vendor-picked" }; + await _service.PrepareVendorReviewModel(listModel, 1, 10); + + _vendorServiceMock.Verify(v => v.GetAllVendorReviews("", null, null, null, null, "vendor-picked", 0, 10), Times.Once); + } + + [TestMethod] + public async Task ApproveVendorReviews_GlobalScope_ApprovesRegardlessOfVendor() + { + var review = new VendorReview { Id = "review-1", VendorId = "vendor-1", IsApproved = false }; + _vendorServiceMock.Setup(v => v.GetVendorReviewById("review-1")).ReturnsAsync(review); + _vendorServiceMock.Setup(v => v.GetVendorById("vendor-1")).ReturnsAsync(new Vendor { Id = "vendor-1" }); + var globalScope = new GlobalAdminDataScope(); + + await _service.ApproveVendorReviews(new[] { "review-1:vendor-1" }, globalScope); + + Assert.IsTrue(review.IsApproved); + _vendorServiceMock.Verify(v => v.UpdateVendorReview(review), Times.Once); + } + + [TestMethod] + public async Task ApproveVendorReviews_VendorScope_SkipsOtherVendorsReview() + { + var review = new VendorReview { Id = "review-1", VendorId = "vendor-OTHER", IsApproved = false }; + _vendorServiceMock.Setup(v => v.GetVendorReviewById("review-1")).ReturnsAsync(review); + var workContextMock = new Mock(); + workContextMock.Setup(w => w.CurrentVendor).Returns(new Vendor { Id = "vendor-mine" }); + var contextAccessorMock = new Mock(); + contextAccessorMock.Setup(c => c.WorkContext).Returns(workContextMock.Object); + var vendorScope = new VendorVendorReviewDataScope(contextAccessorMock.Object); + + await _service.ApproveVendorReviews(new[] { "review-1:vendor-OTHER" }, vendorScope); + + Assert.IsFalse(review.IsApproved); + _vendorServiceMock.Verify(v => v.UpdateVendorReview(It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task DisapproveVendorReviews_VendorScope_SkipsOtherVendorsReview() + { + var review = new VendorReview { Id = "review-1", VendorId = "vendor-OTHER", IsApproved = true }; + _vendorServiceMock.Setup(v => v.GetVendorReviewById("review-1")).ReturnsAsync(review); + var workContextMock = new Mock(); + workContextMock.Setup(w => w.CurrentVendor).Returns(new Vendor { Id = "vendor-mine" }); + var contextAccessorMock = new Mock(); + contextAccessorMock.Setup(c => c.WorkContext).Returns(workContextMock.Object); + var vendorScope = new VendorVendorReviewDataScope(contextAccessorMock.Object); + + await _service.DisapproveVendorReviews(new[] { "review-1:vendor-OTHER" }, vendorScope); + + Assert.IsTrue(review.IsApproved); + _vendorServiceMock.Verify(v => v.UpdateVendorReview(It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task ApproveVendorReviews_MismatchedCompositeVendorId_UsesEntityVendorIdNotClientSuppliedId() + { + var review = new VendorReview { Id = "review-1", VendorId = "vendor-1", IsApproved = false }; + _vendorServiceMock.Setup(v => v.GetVendorReviewById("review-1")).ReturnsAsync(review); + _vendorServiceMock.Setup(v => v.GetVendorById(It.IsAny())).ReturnsAsync(new Vendor { Id = "vendor-1" }); + var globalScope = new GlobalAdminDataScope(); + + await _service.ApproveVendorReviews(new[] { "review-1:vendor-WRONG" }, globalScope); + + _vendorServiceMock.Verify(v => v.GetVendorById("vendor-1"), Times.Once); + _vendorServiceMock.Verify(v => v.GetVendorById("vendor-WRONG"), Times.Never); + } + + [TestMethod] + public async Task DisapproveVendorReviews_MismatchedCompositeVendorId_UsesEntityVendorIdNotClientSuppliedId() + { + var review = new VendorReview { Id = "review-1", VendorId = "vendor-1", IsApproved = true }; + _vendorServiceMock.Setup(v => v.GetVendorReviewById("review-1")).ReturnsAsync(review); + _vendorServiceMock.Setup(v => v.GetVendorById(It.IsAny())).ReturnsAsync(new Vendor { Id = "vendor-1" }); + var globalScope = new GlobalAdminDataScope(); + + await _service.DisapproveVendorReviews(new[] { "review-1:vendor-WRONG" }, globalScope); + + _vendorServiceMock.Verify(v => v.GetVendorById("vendor-1"), Times.Once); + _vendorServiceMock.Verify(v => v.GetVendorById("vendor-WRONG"), Times.Never); + } +} diff --git a/src/Tests/Grand.Web.Vendor.Tests/Controllers/VendorReviewControllerSurfaceTests.cs b/src/Tests/Grand.Web.Vendor.Tests/Controllers/VendorReviewControllerSurfaceTests.cs new file mode 100644 index 000000000..7c93a9840 --- /dev/null +++ b/src/Tests/Grand.Web.Vendor.Tests/Controllers/VendorReviewControllerSurfaceTests.cs @@ -0,0 +1,42 @@ +using Grand.Web.Common.Filters; +using Grand.Web.Vendor.Controllers; +using Grand.Web.Vendor.Extensions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Grand.Web.Vendor.Tests.Controllers; + +[TestClass] +public class VendorReviewControllerSurfaceTests +{ + // Regression guard for the defect class Task 17 caught: BaseVendorReviewController can't carry + // a host's [Area]/[Authorize*] attributes itself (they differ per host), so each concrete + // subclass must restate its own - a missing one here would 404 or deauthorize the whole + // controller silently. Same shape as OrderControllerSurfaceTests (Vendor). + [TestMethod] + public void VendorVendorReviewController_HasAreaAttributeWithVendorArea() + { + var areaAttr = (AreaAttribute)Attribute.GetCustomAttribute(typeof(VendorReviewController), typeof(AreaAttribute), false); + Assert.IsNotNull(areaAttr, "Missing [Area]."); + Assert.AreEqual(Constants.AreaVendor, areaAttr.RouteValue); + } + + [TestMethod] + public void VendorVendorReviewController_HasAuthorizeVendorAttribute() => + Assert.IsTrue(typeof(VendorReviewController).IsDefined(typeof(AuthorizeVendorAttribute), false), + "Missing [AuthorizeVendor]."); + + // Inverse of AdminVendorReviewController_DeclaresVendorSearchAutoComplete: this action leaks + // other vendors' names/ids and must never be promoted or duplicated onto Vendor's controller. + [TestMethod] + public void VendorVendorReviewController_DoesNotDeclareVendorSearchAutoComplete() + { + var declaredMethodNames = typeof(VendorReviewController) + .GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance) + .Select(m => m.Name) + .ToHashSet(); + + Assert.IsFalse(declaredMethodNames.Contains("VendorSearchAutoComplete"), + "Vendor's VendorReviewController must not expose VendorSearchAutoComplete."); + } +} diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/VendorReview/Partials/WidgetZone.DetailsButtons.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/VendorReview/Partials/WidgetZone.DetailsButtons.cshtml new file mode 100644 index 000000000..575fba5c6 --- /dev/null +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/VendorReview/Partials/WidgetZone.DetailsButtons.cshtml @@ -0,0 +1,2 @@ +@model VendorReviewModel + diff --git a/src/Web/Grand.Web.Admin/Controllers/VendorReviewController.cs b/src/Web/Grand.Web.Admin/Controllers/VendorReviewController.cs index ed1a18818..a9e1105c3 100644 --- a/src/Web/Grand.Web.Admin/Controllers/VendorReviewController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/VendorReviewController.cs @@ -1,154 +1,37 @@ -using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Customers; using Grand.Domain.Permissions; +using Grand.Web.AdminShared.Controllers; using Grand.Web.AdminShared.Interfaces; -using Grand.Web.AdminShared.Models.Vendors; -using Grand.Web.Common.DataSource; -using Grand.Web.Common.Filters; +using Grand.Domain.Vendors; using Grand.Web.Common.Security.Authorization; +using Grand.Web.Common.Filters; +using Grand.Web.Admin.Extensions; using Microsoft.AspNetCore.Mvc; namespace Grand.Web.Admin.Controllers; +[AuthorizeAdmin] +[Area(Constants.AreaAdmin)] +[AuthorizeMenu] [PermissionAuthorize(PermissionSystemName.VendorReviews)] -public class VendorReviewController : BaseAdminController +[AutoValidateAntiforgeryToken] +public class VendorReviewController( + IVendorViewModelService vendorViewModelService, + IVendorService vendorService, + ITranslationService translationService, + IAdminDataScope scope) + : BaseVendorReviewController(vendorViewModelService, vendorService, translationService, scope) { - #region Constructors - - public VendorReviewController( - IVendorViewModelService vendorViewModelService, - IVendorService vendorService, - ITranslationService translationService) - { - _vendorViewModelService = vendorViewModelService; - _vendorService = vendorService; - _translationService = translationService; - } - - #endregion - - #region Fields - - private readonly IVendorViewModelService _vendorViewModelService; - private readonly IVendorService _vendorService; - private readonly ITranslationService _translationService; - - #endregion Fields - - #region Methods - - //list - public IActionResult Index() - { - return RedirectToAction("List"); - } - - public IActionResult List() - { - var model = new VendorReviewListModel(); - return View(model); - } - - [PermissionAuthorizeAction(PermissionActionName.List)] - [HttpPost] - public async Task List(DataSourceRequest command, VendorReviewListModel model) - { - var (vendorReviewModels, totalCount) = - await _vendorViewModelService.PrepareVendorReviewModel(model, command.Page, command.PageSize); - var gridModel = new DataSourceResult { - Data = vendorReviewModels.ToList(), - Total = totalCount - }; - - return Json(gridModel); - } - - //edit - [PermissionAuthorizeAction(PermissionActionName.Preview)] - public async Task Edit(string id) - { - var vendorReview = await _vendorService.GetVendorReviewById(id); - - if (vendorReview == null) - //No vendor review found with the specified id - return RedirectToAction("List"); - - var model = new VendorReviewModel(); - await _vendorViewModelService.PrepareVendorReviewModel(model, vendorReview, false, false); - return View(model); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - [HttpPost] - [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")] - public async Task Edit(VendorReviewModel model, bool continueEditing) - { - var vendorReview = await _vendorService.GetVendorReviewById(model.Id); - if (vendorReview == null) - //No vendor review found with the specified id - return RedirectToAction("List"); - - if (ModelState.IsValid) - { - vendorReview = await _vendorViewModelService.UpdateVendorReviewModel(vendorReview, model); - Success(_translationService.GetResource("Admin.VendorReviews.Updated")); - return continueEditing - ? RedirectToAction("Edit", new { id = vendorReview.Id, vendorReview.VendorId }) - : RedirectToAction("List"); - } - - //If we got this far, something failed, redisplay form - await _vendorViewModelService.PrepareVendorReviewModel(model, vendorReview, true, false); - return View(model); - } - - //delete - [PermissionAuthorizeAction(PermissionActionName.Delete)] - [HttpPost] - public async Task Delete(string id) - { - var vendorReview = await _vendorService.GetVendorReviewById(id); - if (vendorReview == null) - //No vendor review found with the specified id - return RedirectToAction("List"); - - if (ModelState.IsValid) - { - await _vendorViewModelService.DeleteVendorReview(vendorReview); - - Success(_translationService.GetResource("Admin.VendorReviews.Deleted")); - return RedirectToAction("List"); - } - - Error(ModelState); - return RedirectToAction("Edit", new { id }); - } - - [HttpPost] - [PermissionAuthorizeAction(PermissionActionName.Edit)] - public async Task ApproveSelected(ICollection selectedIds) - { - if (selectedIds != null) await _vendorViewModelService.ApproveVendorReviews(selectedIds.ToList()); - return Json(new { Result = true }); - } - - [HttpPost] - [PermissionAuthorizeAction(PermissionActionName.Edit)] - public async Task DisapproveSelected(ICollection selectedIds) - { - if (selectedIds != null) await _vendorViewModelService.DisapproveVendorReviews(selectedIds.ToList()); - - return Json(new { Result = true }); - } - + // Not duplicated by Vendor — exposing this to Vendor would leak other vendors' names/ids to a + // vendor account (see ARCH-001 Phase 10 spec §2.2). Kept here rather than on the shared base. public async Task VendorSearchAutoComplete(string term) { const int searchTermMinimumLength = 3; if (string.IsNullOrWhiteSpace(term) || term.Length < searchTermMinimumLength) return Content(""); - var vendors = await _vendorService.SearchVendors( - keywords: term); + var vendors = await VendorService.SearchVendors(keywords: term); var result = (from p in vendors select new @@ -159,6 +42,4 @@ public async Task VendorSearchAutoComplete(string term) .ToList(); return Json(result); } - - #endregion -} \ No newline at end of file +} diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseVendorReviewController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseVendorReviewController.cs new file mode 100644 index 000000000..b2cf5e2a8 --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseVendorReviewController.cs @@ -0,0 +1,147 @@ +using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Interfaces.Customers; +using Grand.Domain.Permissions; +using Grand.Domain.Vendors; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Vendors; +using Grand.Web.Common.Controllers; +using Grand.Web.Common.DataSource; +using Grand.Web.Common.Filters; +using Grand.Web.Common.Security.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Grand.Web.AdminShared.Controllers; + +[PermissionAuthorize(PermissionSystemName.VendorReviews)] +[AutoValidateAntiforgeryToken] +public abstract class BaseVendorReviewController( + IVendorViewModelService vendorViewModelService, + IVendorService vendorService, + ITranslationService translationService, + IAdminDataScope scope) + : BaseController +{ + // Exposed for host-specific concrete subclasses (Admin's VendorSearchAutoComplete action needs + // vendorService — primary-constructor parameters aren't visible to derived classes by name in + // C#). + protected IVendorViewModelService VendorViewModelService => vendorViewModelService; + protected IVendorService VendorService => vendorService; + protected ITranslationService TranslationService => translationService; + protected IAdminDataScope Scope => scope; + + /// DRY replacement for the repeated "load vendor review, redirect to List if not found + /// or not authorized" pattern found in both original controllers. Not a behavior change — every + /// call site below still individually returns RedirectToAction("List") exactly as the + /// originals did. + private async Task<(VendorReview vendorReview, IActionResult denied)> LoadAuthorizedVendorReview(string id) + { + var vendorReview = await vendorService.GetVendorReviewById(id); + if (vendorReview == null) return (null, RedirectToAction("List")); + if (!await scope.HasAccess(vendorReview)) return (null, RedirectToAction("List")); + return (vendorReview, null); + } + + #region VendorReviews + + public IActionResult Index() + { + return RedirectToAction("List"); + } + + public IActionResult List() + { + var model = new VendorReviewListModel(); + return View(model); + } + + [PermissionAuthorizeAction(PermissionActionName.List)] + [HttpPost] + public async Task List(DataSourceRequest command, VendorReviewListModel model) + { + // Scope forces the caller's SearchVendorId only when the host is vendor-scoped (mirrors + // BaseShipmentController.ShipmentListSelect's identical idiom): Admin (DefaultVendorId == + // null) passes through whatever the caller/picker supplied; Vendor (DefaultVendorId == + // CurrentVendor.Id) always forces its own id, reproducing today's hardcoded behavior. + if (scope.DefaultVendorId is not null) model.SearchVendorId = scope.DefaultVendorId; + + var (vendorReviewModels, totalCount) = + await vendorViewModelService.PrepareVendorReviewModel(model, command.Page, command.PageSize); + var gridModel = new DataSourceResult { + Data = vendorReviewModels.ToList(), + Total = totalCount + }; + + return Json(gridModel); + } + + [PermissionAuthorizeAction(PermissionActionName.Preview)] + public async Task Edit(string id) + { + var (vendorReview, denied) = await LoadAuthorizedVendorReview(id); + if (denied != null) return denied; + + var model = new VendorReviewModel(); + await vendorViewModelService.PrepareVendorReviewModel(model, vendorReview, false, false); + return View(model); + } + + [PermissionAuthorizeAction(PermissionActionName.Edit)] + [HttpPost] + [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")] + public async Task Edit(VendorReviewModel model, bool continueEditing) + { + var (vendorReview, denied) = await LoadAuthorizedVendorReview(model.Id); + if (denied != null) return denied; + + if (ModelState.IsValid) + { + vendorReview = await vendorViewModelService.UpdateVendorReviewModel(vendorReview, model); + Success(translationService.GetResource("Admin.VendorReviews.Updated")); + return continueEditing + ? RedirectToAction("Edit", new { id = vendorReview.Id, vendorReview.VendorId }) + : RedirectToAction("List"); + } + + //If we got this far, something failed, redisplay form + await vendorViewModelService.PrepareVendorReviewModel(model, vendorReview, true, false); + return View(model); + } + + [PermissionAuthorizeAction(PermissionActionName.Delete)] + [HttpPost] + public async Task Delete(string id) + { + var (vendorReview, denied) = await LoadAuthorizedVendorReview(id); + if (denied != null) return denied; + + if (ModelState.IsValid) + { + await vendorViewModelService.DeleteVendorReview(vendorReview); + + Success(translationService.GetResource("Admin.VendorReviews.Deleted")); + return RedirectToAction("List"); + } + + Error(ModelState); + return RedirectToAction("Edit", new { id }); + } + + [HttpPost] + [PermissionAuthorizeAction(PermissionActionName.Edit)] + public async Task ApproveSelected(ICollection selectedIds) + { + if (selectedIds != null) await vendorViewModelService.ApproveVendorReviews(selectedIds.ToList(), scope); + return Json(new { Result = true }); + } + + [HttpPost] + [PermissionAuthorizeAction(PermissionActionName.Edit)] + public async Task DisapproveSelected(ICollection selectedIds) + { + if (selectedIds != null) await vendorViewModelService.DisapproveVendorReviews(selectedIds.ToList(), scope); + + return Json(new { Result = true }); + } + + #endregion +} diff --git a/src/Web/Grand.Web.AdminShared/Interfaces/IVendorViewModelService.cs b/src/Web/Grand.Web.AdminShared/Interfaces/IVendorViewModelService.cs index b991a1756..d30fc91e2 100644 --- a/src/Web/Grand.Web.AdminShared/Interfaces/IVendorViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Interfaces/IVendorViewModelService.cs @@ -26,6 +26,6 @@ Task PrepareVendorReviewModel(VendorReviewModel model, Task UpdateVendorReviewModel(VendorReview vendorReview, VendorReviewModel model); Task DeleteVendorReview(VendorReview vendorReview); - Task ApproveVendorReviews(IEnumerable selectedIds); - Task DisapproveVendorReviews(IEnumerable selectedIds); + Task ApproveVendorReviews(IEnumerable selectedIds, IAdminDataScope scope); + Task DisapproveVendorReviews(IEnumerable selectedIds, IAdminDataScope scope); } \ No newline at end of file diff --git a/src/Web/Grand.Web.AdminShared/Services/RoutedVendorReviewDataScope.cs b/src/Web/Grand.Web.AdminShared/Services/RoutedVendorReviewDataScope.cs new file mode 100644 index 000000000..76ef9690c --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Services/RoutedVendorReviewDataScope.cs @@ -0,0 +1,50 @@ +#nullable enable + +using Grand.Domain.Vendors; +using Grand.Web.AdminShared.Interfaces; +using Microsoft.AspNetCore.Http; + +namespace Grand.Web.AdminShared.Services; + +/// +/// Resolves the correct per-host implementation at +/// request time, based on the current request's "area" route value — same fix and same reason as +/// (see that file's doc comment): Grand.Web (the combined +/// host) loads Admin, Store, and Vendor together in one DI container, so a plain +/// AddScoped<IAdminDataScope<VendorReview>, X>() per host would silently let whichever +/// host's StartupApplication ran last win for every area in that process. +/// +/// Unlike Category/Collection/PaymentTransaction (Admin/Store, no Vendor branch), this is the +/// mirror-image 2-branch case: Admin/Vendor, no Store branch — VendorReview has no Store screen +/// at all. +/// +public class RoutedVendorReviewDataScope( + IHttpContextAccessor httpContextAccessor, + GlobalAdminDataScope adminScope, + VendorVendorReviewDataScope vendorScope) : IAdminDataScope +{ + private IAdminDataScope Resolved + { + get + { + var area = httpContextAccessor.HttpContext?.Request.RouteValues["area"] as string; + return area switch { + "Admin" => adminScope, + "Vendor" => vendorScope, + //fail closed: this object fronts vendor tenant isolation, so an unrecognized or + //missing area (including "Store" - VendorReview has no Store screen) must never + //silently resolve to the unscoped global scope + _ => throw new InvalidOperationException( + $"RoutedVendorReviewDataScope: unrecognized or missing area '{area}'.") + }; + } + } + + public Task HasAccess(VendorReview entity) => Resolved.HasAccess(entity); + public Task CanView(VendorReview entity) => Resolved.CanView(entity); + public string? DefaultStoreId => Resolved.DefaultStoreId; + public string ResourceKeyPrefix => Resolved.ResourceKeyPrefix; + public bool ShowStoreSelector => Resolved.ShowStoreSelector; + public string? DefaultVendorId => Resolved.DefaultVendorId; + public bool CanFeatureOnHomepage => Resolved.CanFeatureOnHomepage; +} diff --git a/src/Web/Grand.Web.AdminShared/Services/VendorVendorReviewDataScope.cs b/src/Web/Grand.Web.AdminShared/Services/VendorVendorReviewDataScope.cs new file mode 100644 index 000000000..1f352f42e --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Services/VendorVendorReviewDataScope.cs @@ -0,0 +1,32 @@ +#nullable enable + +using Grand.Domain.Vendors; +using Grand.Infrastructure; +using Grand.Web.AdminShared.Interfaces; + +namespace Grand.Web.AdminShared.Services; + +/// +/// Vendor's . Bespoke: ownership is a flat +/// VendorId field directly on the entity, the same shape as +/// /'s simpler sibling +/// (no child-collection scan, no +/// override needed). Reimplements the existing +/// Grand.Web.Vendor/Extensions/HasAccess.cs's HasAccessToVendorReview inline rather +/// than calling it — Grand.Web.AdminShared has no project reference to +/// Grand.Web.Vendor (the reference direction is the other way), the same constraint every +/// prior Vendor-scope class already works around. +/// +public class VendorVendorReviewDataScope(IContextAccessor contextAccessor) : IAdminDataScope +{ + public Task HasAccess(VendorReview entity) => + Task.FromResult(entity is not null && + !string.IsNullOrEmpty(contextAccessor.WorkContext.CurrentVendor.Id) && + entity.VendorId == contextAccessor.WorkContext.CurrentVendor.Id); + + public string? DefaultStoreId => null; + public string ResourceKeyPrefix => "Vendor"; + public bool ShowStoreSelector => false; + public string? DefaultVendorId => contextAccessor.WorkContext.CurrentVendor.Id; + public bool CanFeatureOnHomepage => false; +} diff --git a/src/Web/Grand.Web.AdminShared/Services/VendorViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/VendorViewModelService.cs index eae43ce80..34376b3e5 100644 --- a/src/Web/Grand.Web.AdminShared/Services/VendorViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/VendorViewModelService.cs @@ -57,7 +57,7 @@ public virtual async Task PrepareVendorReviewModel(VendorReviewModel model, model.Id = vendorReview.Id; model.VendorId = vendorReview.VendorId; - model.VendorName = vendor.Name; + model.VendorName = vendor?.Name; model.CustomerId = vendorReview.CustomerId; model.CustomerInfo = customer != null ? !string.IsNullOrEmpty(customer.Email) @@ -331,43 +331,38 @@ public virtual async Task DeleteVendorReview(VendorReview vendorReview) await vendorService.UpdateVendorReviewTotals(vendor); } - public virtual async Task ApproveVendorReviews(IEnumerable selectedIds) + public virtual async Task ApproveVendorReviews(IEnumerable selectedIds, IAdminDataScope scope) { foreach (var id in selectedIds) { var idReview = id.Split(':').First(); - var idVendor = id.Split(':').Last(); - var vendor = await vendorService.GetVendorById(idVendor); var vendorReview = await vendorService.GetVendorReviewById(idReview); - if (vendorReview != null) - { - var previousIsApproved = vendorReview.IsApproved; - vendorReview.IsApproved = true; - await vendorService.UpdateVendorReview(vendorReview); - await vendorService.UpdateVendorReviewTotals(vendor); - - //raise event (only if it wasn't approved before) - if (!previousIsApproved) - await mediator.Publish(new VendorReviewApprovedEvent(vendorReview)); - } + if (vendorReview == null || !await scope.HasAccess(vendorReview)) continue; + + var vendor = await vendorService.GetVendorById(vendorReview.VendorId); + var previousIsApproved = vendorReview.IsApproved; + vendorReview.IsApproved = true; + await vendorService.UpdateVendorReview(vendorReview); + await vendorService.UpdateVendorReviewTotals(vendor); + + //raise event (only if it wasn't approved before) + if (!previousIsApproved) + await mediator.Publish(new VendorReviewApprovedEvent(vendorReview)); } } - public virtual async Task DisapproveVendorReviews(IEnumerable selectedIds) + public virtual async Task DisapproveVendorReviews(IEnumerable selectedIds, IAdminDataScope scope) { foreach (var id in selectedIds) { var idReview = id.Split(':').First(); - var idVendor = id.Split(':').Last(); - - var vendor = await vendorService.GetVendorById(idVendor); var vendorReview = await vendorService.GetVendorReviewById(idReview); - if (vendorReview != null) - { - vendorReview.IsApproved = false; - await vendorService.UpdateVendorReview(vendorReview); - await vendorService.UpdateVendorReviewTotals(vendor); - } + if (vendorReview == null || !await scope.HasAccess(vendorReview)) continue; + + var vendor = await vendorService.GetVendorById(vendorReview.VendorId); + vendorReview.IsApproved = false; + await vendorService.UpdateVendorReview(vendorReview); + await vendorService.UpdateVendorReviewTotals(vendor); } } } \ No newline at end of file diff --git a/src/Web/Grand.Web.AdminShared/Startup/StartupApplication.cs b/src/Web/Grand.Web.AdminShared/Startup/StartupApplication.cs index 6d67af6f5..395d03e52 100644 --- a/src/Web/Grand.Web.AdminShared/Startup/StartupApplication.cs +++ b/src/Web/Grand.Web.AdminShared/Startup/StartupApplication.cs @@ -4,6 +4,7 @@ using Grand.Domain.Orders; using Grand.Domain.Payments; using Grand.Domain.Shipping; +using Grand.Domain.Vendors; using Grand.Infrastructure; using Grand.Web.AdminShared.Interfaces; using Grand.Web.AdminShared.Services; @@ -123,6 +124,15 @@ public void ConfigureServices(IServiceCollection services, IConfiguration config services.AddScoped(); services.AddScoped(); services.AddScoped(); + + // IAdminDataScope: registered once here for the same reason as + // Product/Category/Collection/Order/Shipment/PaymentTransaction above — see + // RoutedVendorReviewDataScope's doc comment. No Store scope: VendorReview has no Store + // screen. Admin reuses the generic GlobalAdminDataScope unmodified (no restriction on + // Admin's original controller). + services.AddScoped>(); + services.AddScoped(); + services.AddScoped, RoutedVendorReviewDataScope>(); } public void Configure(WebApplication application, IWebHostEnvironment webHostEnvironment) diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/VendorReview/Edit.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/VendorReview/Edit.cshtml similarity index 60% rename from src/Web/Grand.Web.Admin/Areas/Admin/Views/VendorReview/Edit.cshtml rename to src/Web/Grand.Web.AdminShared/Views/AdminShared/VendorReview/Edit.cshtml index a806d40aa..26d5d71a8 100644 --- a/src/Web/Grand.Web.Admin/Areas/Admin/Views/VendorReview/Edit.cshtml +++ b/src/Web/Grand.Web.AdminShared/Views/AdminShared/VendorReview/Edit.cshtml @@ -1,10 +1,13 @@ -@model VendorReviewModel -@inject IContextAccessor contextAccessor +@using Grand.Domain.Vendors +@using Grand.Web.AdminShared.Models.Vendors +@model VendorReviewModel +@inject IAdminDataScope Scope @{ //page title - ViewBag.Title = Loc["Admin.VendorReviews.EditVendorReviewDetails"]; + var area = ViewContext.RouteData.Values["area"]?.ToString(); + ViewBag.Title = Loc[$"{Scope.ResourceKeyPrefix}.VendorReviews.EditVendorReviewDetails"]; } -
+
@@ -12,23 +15,23 @@
- @Loc["Admin.VendorReviews.EditVendorReviewDetails"] + @Loc[$"{Scope.ResourceKeyPrefix}.VendorReviews.EditVendorReviewDetails"] - @Html.ActionLink(Loc["Admin.VendorReviews.BackToList"], "List") + @Html.ActionLink(Loc[$"{Scope.ResourceKeyPrefix}.VendorReviews.BackToList"], "List")
- @Loc["Admin.Common.Delete"] + @Loc[$"{Scope.ResourceKeyPrefix}.Common.Delete"] - +
@@ -39,4 +42,4 @@
- \ No newline at end of file + diff --git a/src/Web/Grand.Web.Vendor/Areas/Vendor/Views/VendorReview/Edit.cshtml b/src/Web/Grand.Web.Vendor/Areas/Vendor/Views/VendorReview/Edit.cshtml deleted file mode 100644 index 670e06936..000000000 --- a/src/Web/Grand.Web.Vendor/Areas/Vendor/Views/VendorReview/Edit.cshtml +++ /dev/null @@ -1,41 +0,0 @@ -@model VendorReviewModel -@{ - //page title - ViewBag.Title = Loc["Vendor.VendorReviews.EditVendorReviewDetails"]; -} -
- -
-
-
-
-
- - @Loc["Vendor.VendorReviews.EditVendorReviewDetails"] - - @Html.ActionLink(Loc["Vendor.VendorReviews.BackToList"], "List") - -
-
-
- - - - @Loc["Vendor.Common.Delete"] - - -
-
-
-
- -
-
-
-
-
- \ No newline at end of file diff --git a/src/Web/Grand.Web.Vendor/Areas/Vendor/Views/VendorReview/List.cshtml b/src/Web/Grand.Web.Vendor/Areas/Vendor/Views/VendorReview/List.cshtml index 4da4b3baa..05334c27b 100644 --- a/src/Web/Grand.Web.Vendor/Areas/Vendor/Views/VendorReview/List.cshtml +++ b/src/Web/Grand.Web.Vendor/Areas/Vendor/Views/VendorReview/List.cshtml @@ -105,11 +105,11 @@ $(document).ready(function () { scrollable: false, dataBound: onDataBound, columns: [{ - field: "Id", + field: "Ids", headerTemplate: "", headerAttributes: { style: "text-align:center" }, attributes: { style: "text-align:center" }, - template: "", + template: "", width: 50 }, { diff --git a/src/Web/Grand.Web.Vendor/Areas/Vendor/Views/VendorReview/Partials/WidgetZone.DetailsButtons.cshtml b/src/Web/Grand.Web.Vendor/Areas/Vendor/Views/VendorReview/Partials/WidgetZone.DetailsButtons.cshtml new file mode 100644 index 000000000..1cfd4f243 --- /dev/null +++ b/src/Web/Grand.Web.Vendor/Areas/Vendor/Views/VendorReview/Partials/WidgetZone.DetailsButtons.cshtml @@ -0,0 +1,2 @@ +@model VendorReviewModel + diff --git a/src/Web/Grand.Web.Vendor/Areas/Vendor/Views/_ViewImports.cshtml b/src/Web/Grand.Web.Vendor/Areas/Vendor/Views/_ViewImports.cshtml index 9e98a146b..e5b500658 100644 --- a/src/Web/Grand.Web.Vendor/Areas/Vendor/Views/_ViewImports.cshtml +++ b/src/Web/Grand.Web.Vendor/Areas/Vendor/Views/_ViewImports.cshtml @@ -30,12 +30,13 @@ Grand.Web.Vendor.Models.Catalog's own ProductModel etc. are no longer used by any controller/view and importing both would make bare "ProductModel" ambiguous. *@ @using Grand.Web.AdminShared.Models.Catalog; -@* Order/Shipment/MerchandiseReturn views bind to Grand.Web.AdminShared's model families (ARCH-001 - Order, Shipment, and MerchandiseReturn consolidation) - Grand.Web.Vendor.Models.Orders/Shipment/ - MerchandiseReturn's own equivalents are no longer used by any controller/view. This can't be a - blanket "@using Grand.Web.AdminShared.Models.Orders" - that namespace also holds *ReportModel etc. - (see the Reports block below, itself alias-only for the identical reason), so a wildcard import - here would make those bare names ambiguous too. Alias just the types that are actually +@* Order, Shipment, MerchandiseReturn, and VendorReview views bind to Grand.Web.AdminShared's model + families (ARCH-001 Phase 5 Order, Phase 6 Shipment, Phase 8 MerchandiseReturn, Phase 10 + VendorReview consolidation) - Grand.Web.Vendor.Models.Orders/Shipment/VendorReview equivalents are + no longer used by any controller/view. This can't be a blanket + "@using Grand.Web.AdminShared.Models.Orders" - that namespace also holds *ReportModel etc. (see + the Reports block below, itself alias-only for the identical reason), so a wildcard import here + would make those bare names ambiguous too. Alias just the types that are actually consolidated. *@ @using OrderModel = Grand.Web.AdminShared.Models.Orders.OrderModel; @using OrderListModel = Grand.Web.AdminShared.Models.Orders.OrderListModel; @@ -49,11 +50,12 @@ @using MerchandiseReturnModel = Grand.Web.AdminShared.Models.Orders.MerchandiseReturnModel; @using MerchandiseReturnListModel = Grand.Web.AdminShared.Models.Orders.MerchandiseReturnListModel; @using Grand.Web.Vendor.Models.Vendor; -@using Grand.Web.Vendor.Models.VendorReview; +@using VendorReviewModel = Grand.Web.AdminShared.Models.Vendors.VendorReviewModel; +@using VendorReviewListModel = Grand.Web.AdminShared.Models.Vendors.VendorReviewListModel; @* Reports views bind to Grand.Web.AdminShared's report model families (ARCH-001 Reports consolidation, Task 11/12) - Grand.Web.Vendor.Models.Report is deleted entirely, so alias just the 4 types Vendor's 5 Reports views actually reference (same alias-not-blanket-import rationale - as the Order/Shipment/MerchandiseReturn block above - Models.Orders also holds + as the Order/Shipment/MerchandiseReturn/VendorReview block above - Models.Orders also holds MerchandiseReturnModel, which is itself only aliased above rather than blanket-imported, for the same reason: a wildcard "@using Grand.Web.AdminShared.Models.Orders" would make every one of these Reports model names ambiguous too). *@ diff --git a/src/Web/Grand.Web.Vendor/Controllers/VendorReviewController.cs b/src/Web/Grand.Web.Vendor/Controllers/VendorReviewController.cs index b27ea0a9b..a48443fde 100644 --- a/src/Web/Grand.Web.Vendor/Controllers/VendorReviewController.cs +++ b/src/Web/Grand.Web.Vendor/Controllers/VendorReviewController.cs @@ -1,151 +1,24 @@ -using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Customers; using Grand.Domain.Permissions; -using Grand.Infrastructure; -using Grand.Web.Common.DataSource; -using Grand.Web.Common.Filters; +using Grand.Domain.Vendors; +using Grand.Web.AdminShared.Controllers; +using Grand.Web.AdminShared.Interfaces; using Grand.Web.Common.Security.Authorization; +using Grand.Web.Common.Filters; using Grand.Web.Vendor.Extensions; -using Grand.Web.Vendor.Interfaces; -using Grand.Web.Vendor.Models.VendorReview; using Microsoft.AspNetCore.Mvc; namespace Grand.Web.Vendor.Controllers; +[Area(Constants.AreaVendor)] +[AuthorizeVendor] +[AuthorizeMenu] [PermissionAuthorize(PermissionSystemName.VendorReviews)] -public class VendorReviewController : BaseVendorController -{ - #region Constructors - - public VendorReviewController( - IVendorReviewViewModelService vendorReviewViewModelService, - IVendorService vendorService, - ITranslationService translationService, - IContextAccessor contextAccessor) - { - _vendorReviewViewModelService = vendorReviewViewModelService; - _vendorService = vendorService; - _translationService = translationService; - _contextAccessor = contextAccessor; - } - - #endregion - - #region Fields - - private readonly IVendorReviewViewModelService _vendorReviewViewModelService; - private readonly IVendorService _vendorService; - private readonly ITranslationService _translationService; - private readonly IContextAccessor _contextAccessor; - - #endregion Fields - - #region Methods - - //list - public IActionResult Index() - { - return RedirectToAction("List"); - } - - public IActionResult List() - { - var model = new VendorReviewListModel(); - return View(model); - } - - [PermissionAuthorizeAction(PermissionActionName.List)] - [HttpPost] - public async Task List(DataSourceRequest command, VendorReviewListModel model) - { - var (vendorReviewModels, totalCount) = - await _vendorReviewViewModelService.PrepareVendorReviewModel(model, command.Page, command.PageSize); - var gridModel = new DataSourceResult { - Data = vendorReviewModels.ToList(), - Total = totalCount - }; - - return Json(gridModel); - } - - //edit - [PermissionAuthorizeAction(PermissionActionName.Preview)] - public async Task Edit(string id) - { - var vendorReview = await _vendorService.GetVendorReviewById(id); - - if (vendorReview == null || !_contextAccessor.WorkContext.HasAccessToVendorReview(vendorReview)) - //No vendor review found with the specified id - return RedirectToAction("List"); - - var model = new VendorReviewModel(); - await _vendorReviewViewModelService.PrepareVendorReviewModel(model, vendorReview, false, false); - return View(model); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - [HttpPost] - [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")] - public async Task Edit(VendorReviewModel model, bool continueEditing) - { - var vendorReview = await _vendorService.GetVendorReviewById(model.Id); - if (vendorReview == null || !_contextAccessor.WorkContext.HasAccessToVendorReview(vendorReview)) - //No vendor review found with the specified id - return RedirectToAction("List"); - - if (ModelState.IsValid) - { - vendorReview = await _vendorReviewViewModelService.UpdateVendorReviewModel(vendorReview, model); - Success(_translationService.GetResource("Admin.VendorReviews.Updated")); - return continueEditing - ? RedirectToAction("Edit", new { id = vendorReview.Id, vendorReview.VendorId }) - : RedirectToAction("List"); - } - - //If we got this far, something failed, redisplay form - await _vendorReviewViewModelService.PrepareVendorReviewModel(model, vendorReview, true, false); - return View(model); - } - - //delete - [PermissionAuthorizeAction(PermissionActionName.Delete)] - [HttpPost] - public async Task Delete(string id) - { - var vendorReview = await _vendorService.GetVendorReviewById(id); - if (vendorReview == null || !_contextAccessor.WorkContext.HasAccessToVendorReview(vendorReview)) - //No vendor review found with the specified id - return RedirectToAction("List"); - - if (ModelState.IsValid) - { - await _vendorReviewViewModelService.DeleteVendorReview(vendorReview); - - Success(_translationService.GetResource("Admin.VendorReviews.Deleted")); - return RedirectToAction("List"); - } - - Error(ModelState); - return RedirectToAction("Edit", new { id }); - } - - [HttpPost] - [PermissionAuthorizeAction(PermissionActionName.Edit)] - public async Task ApproveSelected(ICollection selectedIds) - { - if (selectedIds != null) await _vendorReviewViewModelService.ApproveVendorReviews(selectedIds.ToList()); - - return Json(new { Result = true }); - } - - [HttpPost] - [PermissionAuthorizeAction(PermissionActionName.Edit)] - public async Task DisapproveSelected(ICollection selectedIds) - { - if (selectedIds != null) await _vendorReviewViewModelService.DisapproveVendorReviews(selectedIds.ToList()); - - return Json(new { Result = true }); - } - - #endregion -} \ No newline at end of file +[AutoValidateAntiforgeryToken] +public class VendorReviewController( + IVendorViewModelService vendorViewModelService, + IVendorService vendorService, + ITranslationService translationService, + IAdminDataScope scope) + : BaseVendorReviewController(vendorViewModelService, vendorService, translationService, scope); diff --git a/src/Web/Grand.Web.Vendor/Extensions/HasAccess.cs b/src/Web/Grand.Web.Vendor/Extensions/HasAccess.cs index d62595943..7cfb2ce86 100644 --- a/src/Web/Grand.Web.Vendor/Extensions/HasAccess.cs +++ b/src/Web/Grand.Web.Vendor/Extensions/HasAccess.cs @@ -1,7 +1,6 @@ using Grand.Domain.Catalog; using Grand.Domain.Orders; using Grand.Domain.Shipping; -using Grand.Domain.Vendors; using Grand.Infrastructure; namespace Grand.Web.Vendor.Extensions; @@ -44,11 +43,4 @@ public static bool HasAccessToShipment(this IWorkContext workContext, Shipment s return shipment.VendorId == workContext.CurrentVendor.Id; } - - public static bool HasAccessToVendorReview(this IWorkContext workContext, VendorReview vendorReview) - { - ArgumentNullException.ThrowIfNull(vendorReview); - - return vendorReview.VendorId == workContext.CurrentVendor.Id; - } } \ No newline at end of file diff --git a/src/Web/Grand.Web.Vendor/Interfaces/IVendorReviewViewModelService.cs b/src/Web/Grand.Web.Vendor/Interfaces/IVendorReviewViewModelService.cs deleted file mode 100644 index 084d3e965..000000000 --- a/src/Web/Grand.Web.Vendor/Interfaces/IVendorReviewViewModelService.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Grand.Domain.Vendors; -using Grand.Web.Vendor.Models.VendorReview; - -namespace Grand.Web.Vendor.Interfaces; - -public interface IVendorReviewViewModelService -{ - Task PrepareVendorReviewModel(VendorReviewModel model, - VendorReview vendorReview, bool excludeProperties, bool formatReviewText); - - Task<(IEnumerable vendorReviewModels, int totalCount)> PrepareVendorReviewModel( - VendorReviewListModel model, int pageIndex, int pageSize); - - Task UpdateVendorReviewModel(VendorReview vendorReview, VendorReviewModel model); - Task DeleteVendorReview(VendorReview vendorReview); - Task ApproveVendorReviews(IEnumerable selectedIds); - Task DisapproveVendorReviews(IEnumerable selectedIds); -} \ No newline at end of file diff --git a/src/Web/Grand.Web.Vendor/Models/VendorReview/VendorReviewListModel.cs b/src/Web/Grand.Web.Vendor/Models/VendorReview/VendorReviewListModel.cs deleted file mode 100644 index d6cc5cded..000000000 --- a/src/Web/Grand.Web.Vendor/Models/VendorReview/VendorReviewListModel.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Grand.Infrastructure.ModelBinding; -using Grand.Infrastructure.Models; -using System.ComponentModel.DataAnnotations; - -namespace Grand.Web.Vendor.Models.VendorReview; - -public class VendorReviewListModel : BaseModel -{ - [GrandResourceDisplayName("Vendor.VendorReviews.List.CreatedOnFrom")] - [UIHint("DateNullable")] - public DateTime? CreatedOnFrom { get; set; } - - [GrandResourceDisplayName("Vendor.VendorReviews.List.CreatedOnTo")] - [UIHint("DateNullable")] - public DateTime? CreatedOnTo { get; set; } - - [GrandResourceDisplayName("Vendor.VendorReviews.List.SearchText")] - public string SearchText { get; set; } -} \ No newline at end of file diff --git a/src/Web/Grand.Web.Vendor/Models/VendorReview/VendorReviewModel.cs b/src/Web/Grand.Web.Vendor/Models/VendorReview/VendorReviewModel.cs deleted file mode 100644 index 533e7388c..000000000 --- a/src/Web/Grand.Web.Vendor/Models/VendorReview/VendorReviewModel.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Grand.Infrastructure.ModelBinding; -using Grand.Infrastructure.Models; - -namespace Grand.Web.Vendor.Models.VendorReview; - -public class VendorReviewModel : BaseEntityModel -{ - [GrandResourceDisplayName("Vendor.VendorReviews.Fields.Customer")] - public string CustomerId { get; set; } - - [GrandResourceDisplayName("Vendor.VendorReviews.Fields.Customer")] - public string CustomerInfo { get; set; } - - [GrandResourceDisplayName("Vendor.VendorReviews.Fields.Title")] - public string Title { get; set; } - - [GrandResourceDisplayName("Vendor.VendorReviews.Fields.ReviewText")] - public string ReviewText { get; set; } - - [GrandResourceDisplayName("Vendor.VendorReviews.Fields.Rating")] - public int Rating { get; set; } - - [GrandResourceDisplayName("Vendor.VendorReviews.Fields.IsApproved")] - public bool IsApproved { get; set; } - - [GrandResourceDisplayName("Vendor.VendorReviews.Fields.CreatedOn")] - public DateTime CreatedOn { get; set; } -} \ No newline at end of file diff --git a/src/Web/Grand.Web.Vendor/Services/VendorReviewViewModelService.cs b/src/Web/Grand.Web.Vendor/Services/VendorReviewViewModelService.cs deleted file mode 100644 index 215f74017..000000000 --- a/src/Web/Grand.Web.Vendor/Services/VendorReviewViewModelService.cs +++ /dev/null @@ -1,144 +0,0 @@ -using Grand.Business.Core.Events.Customers; -using Grand.Business.Core.Interfaces.Common.Directory; -using Grand.Business.Core.Interfaces.Common.Localization; -using Grand.Business.Core.Interfaces.Customers; -using Grand.Domain.Vendors; -using Grand.Infrastructure; -using Grand.SharedKernel.Extensions; -using Grand.Web.Vendor.Extensions; -using Grand.Web.Vendor.Interfaces; -using Grand.Web.Vendor.Models.VendorReview; -using Grand.Mediator; - -namespace Grand.Web.Vendor.Services; - -public class VendorReviewViewModelService : IVendorReviewViewModelService -{ - private readonly ICustomerService _customerService; - private readonly IDateTimeService _dateTimeService; - private readonly IMediator _mediator; - private readonly ITranslationService _translationService; - private readonly IVendorService _vendorService; - private readonly IContextAccessor _contextAccessor; - - public VendorReviewViewModelService( - IContextAccessor contextAccessor, - IVendorService vendorService, - ICustomerService customerService, - ITranslationService translationService, - IDateTimeService dateTimeService, - IMediator mediator) - { - _vendorService = vendorService; - _customerService = customerService; - _translationService = translationService; - _dateTimeService = dateTimeService; - _mediator = mediator; - _contextAccessor = contextAccessor; - } - - public virtual async Task PrepareVendorReviewModel(VendorReviewModel model, - VendorReview vendorReview, bool excludeProperties, bool formatReviewText) - { - ArgumentNullException.ThrowIfNull(model); - ArgumentNullException.ThrowIfNull(vendorReview); - - var customer = await _customerService.GetCustomerById(vendorReview.CustomerId); - - model.Id = vendorReview.Id; - model.CustomerId = vendorReview.CustomerId; - model.CustomerInfo = customer != null - ? !string.IsNullOrEmpty(customer.Email) - ? customer.Email - : _translationService.GetResource("Admin.Customers.Guest") - : ""; - model.Rating = vendorReview.Rating; - model.CreatedOn = _dateTimeService.ConvertToUserTime(vendorReview.CreatedOnUtc, DateTimeKind.Utc); - if (!excludeProperties) - { - model.Title = vendorReview.Title; - model.ReviewText = formatReviewText - ? FormatText.ConvertText(vendorReview.ReviewText) - : vendorReview.ReviewText; - model.IsApproved = vendorReview.IsApproved; - } - } - - public virtual async Task<(IEnumerable vendorReviewModels, int totalCount)> - PrepareVendorReviewModel(VendorReviewListModel model, int pageIndex, int pageSize) - { - DateTime? createdOnFromValue = model.CreatedOnFrom == null - ? null - : _dateTimeService.ConvertToUtcTime(model.CreatedOnFrom.Value, _dateTimeService.CurrentTimeZone); - - DateTime? createdToFromValue = model.CreatedOnTo == null - ? null - : _dateTimeService.ConvertToUtcTime(model.CreatedOnTo.Value, _dateTimeService.CurrentTimeZone).AddDays(1); - - var vendorReviews = await _vendorService.GetAllVendorReviews("", null, - createdOnFromValue, createdToFromValue, model.SearchText, _contextAccessor.WorkContext.CurrentVendor.Id, pageIndex - 1, - pageSize); - var items = new List(); - foreach (var x in vendorReviews) - { - var m = new VendorReviewModel(); - await PrepareVendorReviewModel(m, x, false, true); - items.Add(m); - } - - return (items, vendorReviews.TotalCount); - } - - public virtual async Task UpdateVendorReviewModel(VendorReview vendorReview, VendorReviewModel model) - { - vendorReview.Title = model.Title; - vendorReview.ReviewText = model.ReviewText; - vendorReview.IsApproved = model.IsApproved; - - await _vendorService.UpdateVendorReview(vendorReview); - - var vendor = await _vendorService.GetVendorById(vendorReview.VendorId); - //update vendor totals - await _vendorService.UpdateVendorReviewTotals(vendor); - return vendorReview; - } - - public virtual async Task DeleteVendorReview(VendorReview vendorReview) - { - await _vendorService.DeleteVendorReview(vendorReview); - var vendor = await _vendorService.GetVendorById(vendorReview.VendorId); - //update vendor totals - await _vendorService.UpdateVendorReviewTotals(vendor); - } - - public virtual async Task ApproveVendorReviews(IEnumerable selectedIds) - { - foreach (var id in selectedIds) - { - var vendorReview = await _vendorService.GetVendorReviewById(id); - if (vendorReview == null || !_contextAccessor.WorkContext.HasAccessToVendorReview(vendorReview)) continue; - - var previousIsApproved = vendorReview.IsApproved; - vendorReview.IsApproved = true; - await _vendorService.UpdateVendorReview(vendorReview); - await _vendorService.UpdateVendorReviewTotals(_contextAccessor.WorkContext.CurrentVendor); - - //raise event (only if it wasn't approved before) - if (!previousIsApproved) - await _mediator.Publish(new VendorReviewApprovedEvent(vendorReview)); - } - } - - public virtual async Task DisapproveVendorReviews(IEnumerable selectedIds) - { - foreach (var id in selectedIds) - { - var vendorReview = await _vendorService.GetVendorReviewById(id); - if (vendorReview == null || !_contextAccessor.WorkContext.HasAccessToVendorReview(vendorReview)) continue; - - vendorReview.IsApproved = false; - await _vendorService.UpdateVendorReview(vendorReview); - await _vendorService.UpdateVendorReviewTotals(_contextAccessor.WorkContext.CurrentVendor); - } - } -} \ No newline at end of file diff --git a/src/Web/Grand.Web.Vendor/Startup/StartupApplication.cs b/src/Web/Grand.Web.Vendor/Startup/StartupApplication.cs index cc8715008..116f46329 100644 --- a/src/Web/Grand.Web.Vendor/Startup/StartupApplication.cs +++ b/src/Web/Grand.Web.Vendor/Startup/StartupApplication.cs @@ -1,7 +1,5 @@ using Grand.Data; using Grand.Infrastructure; -using Grand.Web.Vendor.Interfaces; -using Grand.Web.Vendor.Services; namespace Grand.Web.Vendor.Startup; @@ -24,12 +22,14 @@ public void ConfigureServices(IServiceCollection services, IConfiguration config // host too via the IStartupApplication assembly scan in StartupBase, since Vendor references // AdminShared. Registering it again here would just be a redundant duplicate of that line. // IOrderViewModelService is likewise registered by Grand.Web.AdminShared's StartupApplication. - // IShipmentViewModelService is likewise registered by Grand.Web.AdminShared's StartupApplication. // IMerchandiseReturnViewModelService is likewise registered by Grand.Web.AdminShared's // StartupApplication (Vendor's own IMerchandiseReturnViewModelService/ // MerchandiseReturnViewModelService were deleted as part of ARCH-001 MerchandiseReturn // consolidation - this host now consumes the shared AdminShared service). - services.AddScoped(); + // IVendorViewModelService is likewise registered by Grand.Web.AdminShared's StartupApplication + // (Vendor's own IVendorReviewViewModelService/VendorReviewViewModelService were deleted as + // part of ARCH-001 VendorReview consolidation - this host now consumes the shared AdminShared + // service for VendorReview too). } public void Configure(WebApplication application, IWebHostEnvironment webHostEnvironment)