diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseMerchandiseReturnControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseMerchandiseReturnControllerTests.cs new file mode 100644 index 0000000000..61af29ac46 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseMerchandiseReturnControllerTests.cs @@ -0,0 +1,662 @@ +extern alias StoreHost; +extern alias VendorHost; + +using Grand.Business.Core.Interfaces.Checkout.Orders; +using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Domain.Customers; +using Grand.Domain.Orders; +using Grand.Domain.Vendors; +using Grand.Infrastructure; +using Grand.Web.AdminShared.Controllers; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Orders; +using Grand.Web.AdminShared.Services; +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 BaseMerchandiseReturnControllerTests +{ + // BaseMerchandiseReturnController is abstract; minimal subclass so actions can be invoked + // directly. NotFoundOrDeniedForNotesSelect (Task 8) uses the base's default (throw) here - Store's + // own override is tested separately in Task 8's tests. + protected class TestMerchandiseReturnController( + IMerchandiseReturnViewModelService merchandiseReturnViewModelService, + ITranslationService translationService, + IMerchandiseReturnService merchandiseReturnService, + IOrderService orderService, + IAdminDataScope scope) + : BaseMerchandiseReturnController(merchandiseReturnViewModelService, translationService, + merchandiseReturnService, orderService, scope); + + protected TestMerchandiseReturnController _controller; + protected Mock _merchandiseReturnServiceMock; + protected Mock _merchandiseReturnViewModelServiceMock; + protected Mock _orderServiceMock; + protected Mock> _scopeMock; + + [TestInitialize] + public void Setup() + { + _merchandiseReturnServiceMock = new Mock(); + _merchandiseReturnViewModelServiceMock = new Mock(); + _orderServiceMock = new Mock(); + _scopeMock = new Mock>(); + _scopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); + + var translationServiceMock = new Mock(); + translationServiceMock.Setup(t => t.GetResource(It.IsAny())).Returns("resource"); + + _controller = new TestMerchandiseReturnController( + _merchandiseReturnViewModelServiceMock.Object, + translationServiceMock.Object, + _merchandiseReturnServiceMock.Object, + _orderServiceMock.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 ListGet_ReturnsViewWithPrepareReturnRequestListModelResult() + { + var model = new MerchandiseReturnListModel(); + _merchandiseReturnViewModelServiceMock.Setup(v => v.PrepareReturnRequestListModel()).Returns(model); + + var result = _controller.List(); + + var view = result as ViewResult; + Assert.IsNotNull(view); + Assert.AreSame(model, view.Model); + } + + [TestMethod] + public async Task ListPost_StoreScope_ForcesModelStoreId() + { + _scopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); + _merchandiseReturnViewModelServiceMock + .Setup(v => v.PrepareMerchandiseReturnModel(It.IsAny(), 1, 10)) + .ReturnsAsync((new List(), 0)); + + var model = new MerchandiseReturnListModel { StoreId = "attacker-supplied" }; + await _controller.List(new DataSourceRequest { Page = 1, PageSize = 10 }, model); + + Assert.AreEqual("store-1", model.StoreId); + } + + [TestMethod] + public async Task ListPost_GlobalScope_LeavesSubmittedStoreIdUntouched() + { + _scopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); + _merchandiseReturnViewModelServiceMock + .Setup(v => v.PrepareMerchandiseReturnModel(It.IsAny(), 1, 10)) + .ReturnsAsync((new List(), 0)); + + var model = new MerchandiseReturnListModel { StoreId = "admin-submitted" }; + await _controller.List(new DataSourceRequest { Page = 1, PageSize = 10 }, model); + + Assert.AreEqual("admin-submitted", model.StoreId); + } + + [TestMethod] + public async Task ListPost_VendorScope_LeavesStoreIdUntouched_ScopingHappensInsideService() + { + // Vendor has no store concept - List(POST) never forces model.StoreId for Vendor (spec §5); + // vendor-scoping happens inside the shared service call via scope.DefaultVendorId (Task 4). + _scopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); + _scopeMock.Setup(s => s.DefaultVendorId).Returns("vendor-A"); + _merchandiseReturnViewModelServiceMock + .Setup(v => v.PrepareMerchandiseReturnModel(It.IsAny(), 1, 10)) + .ReturnsAsync((new List(), 0)); + + var model = new MerchandiseReturnListModel(); + await _controller.List(new DataSourceRequest { Page = 1, PageSize = 10 }, model); + + Assert.IsNull(model.StoreId); + } + + // --- GoToId -------------------------------------------------------------------------------- + + [TestMethod] + public async Task GoToId_NullGoDirectlyToId_RedirectsToList() + { + var result = await _controller.GoToId(new MerchandiseReturnListModel { GoDirectlyToId = null }); + + var redirect = result as RedirectToActionResult; + Assert.IsNotNull(redirect); + Assert.AreEqual("List", redirect.ActionName); + _merchandiseReturnServiceMock.Verify(s => s.GetMerchandiseReturnById(It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task GoToId_NotFound_RedirectsToList() + { + _merchandiseReturnServiceMock.Setup(s => s.GetMerchandiseReturnById(42)).ReturnsAsync((MerchandiseReturn)null); + + var result = await _controller.GoToId(new MerchandiseReturnListModel { GoDirectlyToId = "42" }); + + var redirect = result as RedirectToActionResult; + Assert.IsNotNull(redirect); + Assert.AreEqual("List", redirect.ActionName); + } + + [TestMethod] + public async Task GoToId_ScopeDenies_RedirectsToList() + { + var entity = new MerchandiseReturn { Id = "mr1" }; + _merchandiseReturnServiceMock.Setup(s => s.GetMerchandiseReturnById(42)).ReturnsAsync(entity); + _scopeMock.Setup(s => s.HasAccess(entity)).ReturnsAsync(false); + + var result = await _controller.GoToId(new MerchandiseReturnListModel { GoDirectlyToId = "42" }); + + var redirect = result as RedirectToActionResult; + Assert.IsNotNull(redirect); + Assert.AreEqual("List", redirect.ActionName); + } + + [TestMethod] + public async Task GoToId_NonNumericString_TreatedAsNotFound() + { + // int.TryParse("abc", out var id) leaves id = 0; GetMerchandiseReturnById(0) returning null + // reproduces "not found" the same way it did before this phase (spec §2.6 - no regression). + _merchandiseReturnServiceMock.Setup(s => s.GetMerchandiseReturnById(0)).ReturnsAsync((MerchandiseReturn)null); + + var result = await _controller.GoToId(new MerchandiseReturnListModel { GoDirectlyToId = "abc" }); + + var redirect = result as RedirectToActionResult; + Assert.IsNotNull(redirect); + Assert.AreEqual("List", redirect.ActionName); + } + + [TestMethod] + public async Task GoToId_Authorized_RedirectsToEdit() + { + var entity = new MerchandiseReturn { Id = "mr1" }; + _merchandiseReturnServiceMock.Setup(s => s.GetMerchandiseReturnById(42)).ReturnsAsync(entity); + _scopeMock.Setup(s => s.HasAccess(entity)).ReturnsAsync(true); + + var result = await _controller.GoToId(new MerchandiseReturnListModel { GoDirectlyToId = "42" }); + + var redirect = result as RedirectToActionResult; + Assert.IsNotNull(redirect); + Assert.AreEqual("Edit", redirect.ActionName); + Assert.AreEqual("mr1", redirect.RouteValues["id"]); + } + + // --- ProductsForMerchandiseReturn ------------------------------------------------------------ + + [TestMethod] + public async Task ProductsForMerchandiseReturn_NotFound_ReturnsKendoError() + { + _merchandiseReturnServiceMock.Setup(s => s.GetMerchandiseReturnById("mr1")).ReturnsAsync((MerchandiseReturn)null); + + var result = await _controller.ProductsForMerchandiseReturn("mr1", new DataSourceRequest()); + + var json = result as JsonResult; + Assert.IsNotNull(json); + var gridModel = (DataSourceResult)json.Value; + Assert.IsFalse(string.IsNullOrEmpty((string)gridModel.Errors)); + } + + [TestMethod] + public async Task ProductsForMerchandiseReturn_ScopeDenies_ReturnsKendoError() + { + var entity = new MerchandiseReturn { Id = "mr1" }; + _merchandiseReturnServiceMock.Setup(s => s.GetMerchandiseReturnById("mr1")).ReturnsAsync(entity); + _scopeMock.Setup(s => s.HasAccess(entity)).ReturnsAsync(false); + + var result = await _controller.ProductsForMerchandiseReturn("mr1", new DataSourceRequest()); + + var json = result as JsonResult; + Assert.IsNotNull(json); + var gridModel = (DataSourceResult)json.Value; + Assert.IsFalse(string.IsNullOrEmpty((string)gridModel.Errors)); + } + + [TestMethod] + public async Task ProductsForMerchandiseReturn_Authorized_ReturnsItems() + { + var entity = new MerchandiseReturn { Id = "mr1" }; + _merchandiseReturnServiceMock.Setup(s => s.GetMerchandiseReturnById("mr1")).ReturnsAsync(entity); + _scopeMock.Setup(s => s.HasAccess(entity)).ReturnsAsync(true); + var items = new List { new() }; + _merchandiseReturnViewModelServiceMock.Setup(v => v.PrepareMerchandiseReturnItemModel("mr1")).ReturnsAsync(items); + + var result = await _controller.ProductsForMerchandiseReturn("mr1", new DataSourceRequest()); + + var json = result as JsonResult; + var gridModel = (DataSourceResult)json.Value; + Assert.AreEqual(1, gridModel.Total); + } + + // --- Edit (GET) ------------------------------------------------------------------------------ + + [TestMethod] + public async Task EditGet_NotFound_RedirectsToList() + { + _merchandiseReturnServiceMock.Setup(s => s.GetMerchandiseReturnById("missing")).ReturnsAsync((MerchandiseReturn)null); + + var result = await _controller.Edit("missing"); + + var redirect = result as RedirectToActionResult; + Assert.IsNotNull(redirect); + Assert.AreEqual("List", redirect.ActionName); + _scopeMock.Verify(s => s.CanView(It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task EditGet_ScopeDeniesView_RedirectsToList() + { + var entity = new MerchandiseReturn { Id = "mr1" }; + _merchandiseReturnServiceMock.Setup(s => s.GetMerchandiseReturnById("mr1")).ReturnsAsync(entity); + _scopeMock.Setup(s => s.CanView(entity)).ReturnsAsync(false); + + var result = await _controller.Edit("mr1"); + + var redirect = result as RedirectToActionResult; + Assert.IsNotNull(redirect); + Assert.AreEqual("List", redirect.ActionName); + } + + [TestMethod] + public async Task EditGet_Authorized_ReturnsViewAndPreparesModel() + { + var entity = new MerchandiseReturn { Id = "mr1" }; + _merchandiseReturnServiceMock.Setup(s => s.GetMerchandiseReturnById("mr1")).ReturnsAsync(entity); + _scopeMock.Setup(s => s.CanView(entity)).ReturnsAsync(true); + + var result = await _controller.Edit("mr1"); + + Assert.IsInstanceOfType(result, typeof(ViewResult)); + _merchandiseReturnViewModelServiceMock.Verify( + v => v.PrepareMerchandiseReturnModel(It.IsAny(), entity, false), Times.Once); + } + + // --- Edit (POST) ----------------------------------------------------------------------------- + + [TestMethod] + public async Task EditPost_NotFound_RedirectsToList() + { + _merchandiseReturnServiceMock.Setup(s => s.GetMerchandiseReturnById("missing")).ReturnsAsync((MerchandiseReturn)null); + var orderSettings = new Grand.Domain.Orders.OrderSettings(); + + var result = await _controller.Edit(new MerchandiseReturnModel { Id = "missing" }, false, + new Mock().Object, + new Mock().Object, + orderSettings); + + var redirect = result as RedirectToActionResult; + Assert.IsNotNull(redirect); + Assert.AreEqual("List", redirect.ActionName); + } + + [TestMethod] + public async Task EditPost_ScopeDenies_RedirectsToList() + { + var entity = new MerchandiseReturn { Id = "mr1" }; + _merchandiseReturnServiceMock.Setup(s => s.GetMerchandiseReturnById("mr1")).ReturnsAsync(entity); + _scopeMock.Setup(s => s.HasAccess(entity)).ReturnsAsync(false); + var orderSettings = new Grand.Domain.Orders.OrderSettings(); + + var result = await _controller.Edit(new MerchandiseReturnModel { Id = "mr1" }, false, + new Mock().Object, + new Mock().Object, + orderSettings); + + var redirect = result as RedirectToActionResult; + Assert.IsNotNull(redirect); + Assert.AreEqual("List", redirect.ActionName); + _merchandiseReturnViewModelServiceMock.Verify( + v => v.UpdateMerchandiseReturnModel(It.IsAny(), It.IsAny(), + It.IsAny>()), Times.Never); + } + + [TestMethod] + public async Task EditPost_ValidAndAuthorized_UpdatesAndRedirectsToList() + { + var entity = new MerchandiseReturn { Id = "mr1" }; + _merchandiseReturnServiceMock.Setup(s => s.GetMerchandiseReturnById("mr1")).ReturnsAsync(entity); + _scopeMock.Setup(s => s.HasAccess(entity)).ReturnsAsync(true); + _merchandiseReturnViewModelServiceMock + .Setup(v => v.UpdateMerchandiseReturnModel(entity, It.IsAny(), It.IsAny>())) + .ReturnsAsync(entity); + var orderSettings = new Grand.Domain.Orders.OrderSettings { MerchandiseReturns_AllowToSpecifyPickupAddress = false }; + + var result = await _controller.Edit(new MerchandiseReturnModel { Id = "mr1" }, false, + new Mock().Object, + new Mock().Object, + orderSettings); + + var redirect = result as RedirectToActionResult; + Assert.IsNotNull(redirect); + Assert.AreEqual("List", redirect.ActionName); + } + + // --- Delete (POST) --------------------------------------------------------------------------- + + [TestMethod] + public async Task Delete_NotFound_RedirectsToList() + { + _merchandiseReturnServiceMock.Setup(s => s.GetMerchandiseReturnById("missing")).ReturnsAsync((MerchandiseReturn)null); + + var result = await _controller.Delete("missing"); + + var redirect = result as RedirectToActionResult; + Assert.IsNotNull(redirect); + Assert.AreEqual("List", redirect.ActionName); + } + + [TestMethod] + public async Task Delete_ScopeDenies_RedirectsToListWithoutDeleting() + { + var entity = new MerchandiseReturn { Id = "mr1" }; + _merchandiseReturnServiceMock.Setup(s => s.GetMerchandiseReturnById("mr1")).ReturnsAsync(entity); + _scopeMock.Setup(s => s.HasAccess(entity)).ReturnsAsync(false); + + var result = await _controller.Delete("mr1"); + + var redirect = result as RedirectToActionResult; + Assert.IsNotNull(redirect); + Assert.AreEqual("List", redirect.ActionName); + _merchandiseReturnViewModelServiceMock.Verify(v => v.DeleteMerchandiseReturn(It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task Delete_Authorized_DeletesAndRedirectsToList() + { + var entity = new MerchandiseReturn { Id = "mr1" }; + _merchandiseReturnServiceMock.Setup(s => s.GetMerchandiseReturnById("mr1")).ReturnsAsync(entity); + _scopeMock.Setup(s => s.HasAccess(entity)).ReturnsAsync(true); + + var result = await _controller.Delete("mr1"); + + var redirect = result as RedirectToActionResult; + Assert.IsNotNull(redirect); + Assert.AreEqual("List", redirect.ActionName); + _merchandiseReturnViewModelServiceMock.Verify(v => v.DeleteMerchandiseReturn(entity), Times.Once); + } + + // --- MerchandiseReturnNotesSelect -------------------------------------------------------------- + + [TestMethod] + public async Task NotesSelect_NotFound_BaseDefault_ThrowsArgumentException() + { + _merchandiseReturnServiceMock.Setup(s => s.GetMerchandiseReturnById("missing")).ReturnsAsync((MerchandiseReturn)null); + + await Assert.ThrowsExactlyAsync( + async () => await _controller.MerchandiseReturnNotesSelect("missing", new DataSourceRequest())); + } + + [TestMethod] + public async Task NotesSelect_ScopeDenies_BaseDefault_ThrowsArgumentException() + { + // Base default is the Admin/Vendor shape (throw) - Store's subclass override is tested in + // Task 10's thin-subclass tests, not here. + var entity = new MerchandiseReturn { Id = "mr1" }; + _merchandiseReturnServiceMock.Setup(s => s.GetMerchandiseReturnById("mr1")).ReturnsAsync(entity); + _scopeMock.Setup(s => s.HasAccess(entity)).ReturnsAsync(false); + + await Assert.ThrowsExactlyAsync( + async () => await _controller.MerchandiseReturnNotesSelect("mr1", new DataSourceRequest())); + } + + [TestMethod] + public async Task NotesSelect_Authorized_ReturnsNotes() + { + var entity = new MerchandiseReturn { Id = "mr1" }; + _merchandiseReturnServiceMock.Setup(s => s.GetMerchandiseReturnById("mr1")).ReturnsAsync(entity); + _scopeMock.Setup(s => s.HasAccess(entity)).ReturnsAsync(true); + var notes = new List { new() }; + _merchandiseReturnViewModelServiceMock.Setup(v => v.PrepareMerchandiseReturnNotes(entity)).ReturnsAsync(notes); + + var result = await _controller.MerchandiseReturnNotesSelect("mr1", new DataSourceRequest()); + + var json = result as JsonResult; + var gridModel = (DataSourceResult)json.Value; + Assert.AreEqual(1, gridModel.Total); + } + + // --- MerchandiseReturnNoteAdd ------------------------------------------------------------------ + + [TestMethod] + public async Task NoteAdd_NotFound_ReturnsJsonResultFalse() + { + _merchandiseReturnServiceMock.Setup(s => s.GetMerchandiseReturnById("missing")).ReturnsAsync((MerchandiseReturn)null); + + var result = await _controller.MerchandiseReturnNoteAdd("missing", null, false, "msg"); + + var json = result as JsonResult; + Assert.IsFalse((bool)json.Value.GetType().GetProperty("Result").GetValue(json.Value)); + _orderServiceMock.Verify(o => o.GetOrderById(It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task NoteAdd_ScopeDenies_ReturnsJsonResultFalse() + { + var entity = new MerchandiseReturn { Id = "mr1", OrderId = "o1" }; + _merchandiseReturnServiceMock.Setup(s => s.GetMerchandiseReturnById("mr1")).ReturnsAsync(entity); + _scopeMock.Setup(s => s.HasAccess(entity)).ReturnsAsync(false); + + var result = await _controller.MerchandiseReturnNoteAdd("mr1", null, false, "msg"); + + var json = result as JsonResult; + Assert.IsFalse((bool)json.Value.GetType().GetProperty("Result").GetValue(json.Value)); + _merchandiseReturnViewModelServiceMock.Verify( + v => v.InsertMerchandiseReturnNote(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + } + + [TestMethod] + public async Task NoteAdd_Authorized_ResolvesOrderFromEntity_NotFromAParameter() + { + // Spec §2.5/§11 (approved): no orderId request parameter exists on this action at all - + // order is always resolved server-side from merchandiseReturn.OrderId. + var entity = new MerchandiseReturn { Id = "mr1", OrderId = "o1" }; + var order = new Order { Id = "o1" }; + _merchandiseReturnServiceMock.Setup(s => s.GetMerchandiseReturnById("mr1")).ReturnsAsync(entity); + _scopeMock.Setup(s => s.HasAccess(entity)).ReturnsAsync(true); + _orderServiceMock.Setup(o => o.GetOrderById("o1")).ReturnsAsync(order); + + var result = await _controller.MerchandiseReturnNoteAdd("mr1", "download-1", true, "msg"); + + var json = result as JsonResult; + Assert.IsTrue((bool)json.Value.GetType().GetProperty("Result").GetValue(json.Value)); + _orderServiceMock.Verify(o => o.GetOrderById("o1"), Times.Once); + _merchandiseReturnViewModelServiceMock.Verify( + v => v.InsertMerchandiseReturnNote(entity, order, "download-1", true, "msg"), Times.Once); + } + + [TestMethod] + public async Task NoteAdd_OrderNotFound_ReturnsJsonResultFalse() + { + // Restores Admin/Store's original null-check on the server-resolved order: without it, a note + // would persist for a merchandise return whose order no longer exists, and (when + // displayToCustomer is true) InsertMerchandiseReturnNote would NRE dereferencing order.StoreId. + var entity = new MerchandiseReturn { Id = "mr1", OrderId = "o1" }; + _merchandiseReturnServiceMock.Setup(s => s.GetMerchandiseReturnById("mr1")).ReturnsAsync(entity); + _scopeMock.Setup(s => s.HasAccess(entity)).ReturnsAsync(true); + _orderServiceMock.Setup(o => o.GetOrderById("o1")).ReturnsAsync((Order)null); + + var result = await _controller.MerchandiseReturnNoteAdd("mr1", null, false, "msg"); + + var json = result as JsonResult; + Assert.IsFalse((bool)json.Value.GetType().GetProperty("Result").GetValue(json.Value)); + _merchandiseReturnViewModelServiceMock.Verify( + v => v.InsertMerchandiseReturnNote(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + } + + // --- MerchandiseReturnNoteDelete --------------------------------------------------------------- + + [TestMethod] + public async Task NoteDelete_NotFound_ThrowsArgumentException() + { + _merchandiseReturnServiceMock.Setup(s => s.GetMerchandiseReturnById("missing")).ReturnsAsync((MerchandiseReturn)null); + + await Assert.ThrowsExactlyAsync( + async () => await _controller.MerchandiseReturnNoteDelete("note1", "missing")); + } + + [TestMethod] + public async Task NoteDelete_ScopeDenies_ReturnsJsonResultFalse_ForEveryHost() + { + // Store's existing check moves here uniformly; Admin/Vendor gain it for free + // (GlobalAdminDataScope's always-true HasAccess keeps Admin unaffected) - spec §5. + var entity = new MerchandiseReturn { Id = "mr1" }; + _merchandiseReturnServiceMock.Setup(s => s.GetMerchandiseReturnById("mr1")).ReturnsAsync(entity); + _scopeMock.Setup(s => s.HasAccess(entity)).ReturnsAsync(false); + + var result = await _controller.MerchandiseReturnNoteDelete("note1", "mr1"); + + var json = result as JsonResult; + Assert.IsFalse((bool)json.Value.GetType().GetProperty("Result").GetValue(json.Value)); + _merchandiseReturnViewModelServiceMock.Verify( + v => v.DeleteMerchandiseReturnNote(It.IsAny(), It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task NoteDelete_Authorized_Deletes() + { + var entity = new MerchandiseReturn { Id = "mr1" }; + _merchandiseReturnServiceMock.Setup(s => s.GetMerchandiseReturnById("mr1")).ReturnsAsync(entity); + _scopeMock.Setup(s => s.HasAccess(entity)).ReturnsAsync(true); + + var result = await _controller.MerchandiseReturnNoteDelete("note1", "mr1"); + + Assert.IsInstanceOfType(result, typeof(JsonResult)); + _merchandiseReturnViewModelServiceMock.Verify(v => v.DeleteMerchandiseReturnNote(entity, "note1"), Times.Once); + } + + [TestMethod] + public async Task NotesSelect_NotFoundOrDenied_StoreSubclass_ReturnsEmptyContent_NoThrow() + { + var storeController = new StoreHost::Grand.Web.Store.Controllers.MerchandiseReturnController( + _merchandiseReturnViewModelServiceMock.Object, + new Mock().Object, + _merchandiseReturnServiceMock.Object, + _orderServiceMock.Object, + _scopeMock.Object); + storeController.ControllerContext = _controller.ControllerContext; + storeController.TempData = _controller.TempData; + + _merchandiseReturnServiceMock.Setup(s => s.GetMerchandiseReturnById("missing")).ReturnsAsync((MerchandiseReturn)null); + + var result = await storeController.MerchandiseReturnNotesSelect("missing", new DataSourceRequest()); + + var content = result as ContentResult; + Assert.IsNotNull(content); + Assert.AreEqual("", content.Content); + } + + [TestMethod] + public async Task Delete_RealGlobalAdminScope_AlwaysAllows() + { + var realScope = new GlobalAdminDataScope(); + var controller = new TestMerchandiseReturnController(_merchandiseReturnViewModelServiceMock.Object, + new Mock().Object, _merchandiseReturnServiceMock.Object, _orderServiceMock.Object, realScope); + controller.ControllerContext = _controller.ControllerContext; + controller.TempData = _controller.TempData; + var entity = new MerchandiseReturn { Id = "mr1", StoreId = "any-store", VendorId = "any-vendor", SeId = "any-se" }; + _merchandiseReturnServiceMock.Setup(s => s.GetMerchandiseReturnById("mr1")).ReturnsAsync(entity); + + var result = await controller.Delete("mr1"); + + Assert.AreEqual("List", ((RedirectToActionResult)result).ActionName); + _merchandiseReturnViewModelServiceMock.Verify(v => v.DeleteMerchandiseReturn(entity), Times.Once); + } + + [TestMethod] + public async Task Delete_RealStoreScope_DeniesCrossStoreEntity() + { + var workContextMock = new Mock(); + workContextMock.Setup(w => w.CurrentCustomer).Returns(new Customer { StaffStoreId = "store-1" }); + var contextAccessorMock = new Mock(); + contextAccessorMock.Setup(c => c.WorkContext).Returns(workContextMock.Object); + var realScope = new StoreMerchandiseReturnDataScope(contextAccessorMock.Object); + + var controller = new TestMerchandiseReturnController(_merchandiseReturnViewModelServiceMock.Object, + new Mock().Object, _merchandiseReturnServiceMock.Object, _orderServiceMock.Object, realScope); + controller.ControllerContext = _controller.ControllerContext; + controller.TempData = _controller.TempData; + var entity = new MerchandiseReturn { Id = "mr1", StoreId = "store-2" }; + _merchandiseReturnServiceMock.Setup(s => s.GetMerchandiseReturnById("mr1")).ReturnsAsync(entity); + + var result = await controller.Delete("mr1"); + + Assert.AreEqual("List", ((RedirectToActionResult)result).ActionName); + _merchandiseReturnViewModelServiceMock.Verify(v => v.DeleteMerchandiseReturn(It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task Delete_RealVendorScope_DeniesCrossVendorEntity() + { + var workContextMock = new Mock(); + workContextMock.Setup(w => w.CurrentVendor).Returns(new Vendor { Id = "vendor-A" }); + var contextAccessorMock = new Mock(); + contextAccessorMock.Setup(c => c.WorkContext).Returns(workContextMock.Object); + var realScope = new VendorMerchandiseReturnDataScope(contextAccessorMock.Object); + + var controller = new TestMerchandiseReturnController(_merchandiseReturnViewModelServiceMock.Object, + new Mock().Object, _merchandiseReturnServiceMock.Object, _orderServiceMock.Object, realScope); + controller.ControllerContext = _controller.ControllerContext; + controller.TempData = _controller.TempData; + var entity = new MerchandiseReturn { Id = "mr1", VendorId = "vendor-B" }; + _merchandiseReturnServiceMock.Setup(s => s.GetMerchandiseReturnById("mr1")).ReturnsAsync(entity); + + var result = await controller.Delete("mr1"); + + Assert.AreEqual("List", ((RedirectToActionResult)result).ActionName); + _merchandiseReturnViewModelServiceMock.Verify(v => v.DeleteMerchandiseReturn(It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task GoToId_RealStoreScope_PositiveControl_OwnStoreEntityResolves() + { + var workContextMock = new Mock(); + workContextMock.Setup(w => w.CurrentCustomer).Returns(new Customer { StaffStoreId = "store-1" }); + var contextAccessorMock = new Mock(); + contextAccessorMock.Setup(c => c.WorkContext).Returns(workContextMock.Object); + var realScope = new StoreMerchandiseReturnDataScope(contextAccessorMock.Object); + + var controller = new TestMerchandiseReturnController(_merchandiseReturnViewModelServiceMock.Object, + new Mock().Object, _merchandiseReturnServiceMock.Object, _orderServiceMock.Object, realScope); + controller.ControllerContext = _controller.ControllerContext; + controller.TempData = _controller.TempData; + var entity = new MerchandiseReturn { Id = "mr1", StoreId = "store-1" }; + _merchandiseReturnServiceMock.Setup(s => s.GetMerchandiseReturnById(7)).ReturnsAsync(entity); + + var result = await controller.GoToId(new MerchandiseReturnListModel { GoDirectlyToId = "7" }); + + var redirect = result as RedirectToActionResult; + Assert.AreEqual("Edit", redirect.ActionName); + } + + [TestMethod] + public void HasAccessToMerchandiseReturn_NoLongerDefinedOnVendorHasAccessExtensions() + { + // Regression guard for the cleanup in Task 10, Step 7 - reflection-based, so it fails loudly + // if the extension method is ever reintroduced instead of relying on scope.HasAccess. + var hasAccessType = typeof(VendorHost::Grand.Web.Vendor.Extensions.HasAccess); + var method = hasAccessType.GetMethod("HasAccessToMerchandiseReturn"); + Assert.IsNull(method, "HasAccessToMerchandiseReturn should have been deleted once every " + + "call site moved onto VendorMerchandiseReturnDataScope.HasAccess (Task 10)."); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/MerchandiseReturnControllerAttributeTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/MerchandiseReturnControllerAttributeTests.cs new file mode 100644 index 0000000000..3d9e52e71d --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/MerchandiseReturnControllerAttributeTests.cs @@ -0,0 +1,116 @@ +extern alias StoreHost; +extern alias VendorHost; + +using Grand.Domain.Permissions; +using Grand.Web.Common.Filters; +using Grand.Web.Common.Security.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Grand.Web.Admin.Tests.Controllers; + +[TestClass] +public class MerchandiseReturnControllerAttributeTests +{ + [TestMethod] + public void AdminController_DerivesFromBaseMerchandiseReturnController() + { + Assert.IsTrue(typeof(Grand.Web.Admin.Controllers.MerchandiseReturnController) + .IsSubclassOf(typeof(Grand.Web.AdminShared.Controllers.BaseMerchandiseReturnController))); + } + + [TestMethod] + public void StoreController_DerivesFromBaseMerchandiseReturnController() + { + Assert.IsTrue(typeof(StoreHost::Grand.Web.Store.Controllers.MerchandiseReturnController) + .IsSubclassOf(typeof(Grand.Web.AdminShared.Controllers.BaseMerchandiseReturnController))); + } + + [TestMethod] + public void VendorController_DerivesFromBaseMerchandiseReturnController() + { + Assert.IsTrue(typeof(VendorHost::Grand.Web.Vendor.Controllers.MerchandiseReturnController) + .IsSubclassOf(typeof(Grand.Web.AdminShared.Controllers.BaseMerchandiseReturnController))); + } + + [TestMethod] + public void BaseController_HasPermissionAuthorizeAttribute_ForMerchandiseReturnsSystemName() + { + var attr = typeof(Grand.Web.AdminShared.Controllers.BaseMerchandiseReturnController) + .GetCustomAttributes(typeof(PermissionAuthorizeAttribute), inherit: false) + .Cast() + .SingleOrDefault(); + Assert.IsNotNull(attr); + Assert.AreEqual(PermissionSystemName.MerchandiseReturns, attr.Permission); + } + + [TestMethod] + public void BaseController_HasAutoValidateAntiforgeryTokenAttribute() + { + var attrs = typeof(Grand.Web.AdminShared.Controllers.BaseMerchandiseReturnController) + .GetCustomAttributes(typeof(Microsoft.AspNetCore.Mvc.AutoValidateAntiforgeryTokenAttribute), inherit: false); + Assert.AreEqual(1, attrs.Length); + } + + // Regression guard for the defect class caught live: BaseMerchandiseReturnController 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 + // (confirmed live: /Store/MerchandiseReturn/List 404'd before this fix). Same shape as + // OrderControllerRoutingTests / PaymentTransactionControllerRoutingTests. + [TestMethod] + public void AdminController_HasAreaAttributeWithAdminArea() + { + var areaAttr = (AreaAttribute)Attribute.GetCustomAttribute( + typeof(Grand.Web.Admin.Controllers.MerchandiseReturnController), typeof(AreaAttribute), false); + Assert.IsNotNull(areaAttr, "Missing [Area]."); + Assert.AreEqual(Grand.Web.Admin.Extensions.Constants.AreaAdmin, areaAttr.RouteValue); + } + + [TestMethod] + public void AdminController_HasAuthorizeAdminAttribute() => + Assert.IsTrue(typeof(Grand.Web.Admin.Controllers.MerchandiseReturnController).IsDefined(typeof(AuthorizeAdminAttribute), false), + "Missing [AuthorizeAdmin]."); + + [TestMethod] + public void AdminController_HasAuthorizeMenuAttribute() => + Assert.IsTrue(typeof(Grand.Web.Admin.Controllers.MerchandiseReturnController).IsDefined(typeof(AuthorizeMenuAttribute), false), + "Missing [AuthorizeMenu]."); + + [TestMethod] + public void StoreController_HasAreaAttributeWithStoreArea() + { + var areaAttr = (AreaAttribute)Attribute.GetCustomAttribute( + typeof(StoreHost::Grand.Web.Store.Controllers.MerchandiseReturnController), typeof(AreaAttribute), false); + Assert.IsNotNull(areaAttr, "Missing [Area]."); + Assert.AreEqual(StoreHost::Grand.Web.Store.Extensions.Constants.AreaStore, areaAttr.RouteValue); + } + + [TestMethod] + public void StoreController_HasAuthorizeStoreAttribute() => + Assert.IsTrue(typeof(StoreHost::Grand.Web.Store.Controllers.MerchandiseReturnController).IsDefined(typeof(AuthorizeStoreAttribute), false), + "Missing [AuthorizeStore]."); + + [TestMethod] + public void StoreController_HasAuthorizeMenuAttribute() => + Assert.IsTrue(typeof(StoreHost::Grand.Web.Store.Controllers.MerchandiseReturnController).IsDefined(typeof(AuthorizeMenuAttribute), false), + "Missing [AuthorizeMenu]."); + + [TestMethod] + public void VendorController_HasAreaAttributeWithVendorArea() + { + var areaAttr = (AreaAttribute)Attribute.GetCustomAttribute( + typeof(VendorHost::Grand.Web.Vendor.Controllers.MerchandiseReturnController), typeof(AreaAttribute), false); + Assert.IsNotNull(areaAttr, "Missing [Area]."); + Assert.AreEqual(VendorHost::Grand.Web.Vendor.Extensions.Constants.AreaVendor, areaAttr.RouteValue); + } + + [TestMethod] + public void VendorController_HasAuthorizeVendorAttribute() => + Assert.IsTrue(typeof(VendorHost::Grand.Web.Vendor.Controllers.MerchandiseReturnController).IsDefined(typeof(AuthorizeVendorAttribute), false), + "Missing [AuthorizeVendor]."); + + [TestMethod] + public void VendorController_HasAuthorizeMenuAttribute() => + Assert.IsTrue(typeof(VendorHost::Grand.Web.Vendor.Controllers.MerchandiseReturnController).IsDefined(typeof(AuthorizeMenuAttribute), false), + "Missing [AuthorizeMenu]."); +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/MerchandiseReturnViewModelServiceTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/MerchandiseReturnViewModelServiceTests.cs new file mode 100644 index 0000000000..0b8725ee5b --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/MerchandiseReturnViewModelServiceTests.cs @@ -0,0 +1,213 @@ +using Grand.Business.Core.Interfaces.Catalog.Prices; +using Grand.Business.Core.Interfaces.Catalog.Products; +using Grand.Business.Core.Interfaces.Checkout.Orders; +using Grand.Business.Core.Interfaces.Common.Addresses; +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Interfaces.Customers; +using Grand.Business.Core.Interfaces.Messages; +using Grand.Business.Core.Interfaces.Storage; +using Grand.Domain; +using Grand.Domain.Common; +using Grand.Domain.Customers; +using Grand.Domain.Directory; +using Grand.Domain.Localization; +using Grand.Domain.Orders; +using Grand.Infrastructure; +using Grand.Infrastructure.Configuration; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Orders; +using Grand.Web.AdminShared.Services; +using Grand.Web.Common.Localization; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Admin.Tests.Controllers; + +[TestClass] +public class MerchandiseReturnViewModelServiceTests +{ + [TestMethod] + public void InterfaceExposesRenamedMethod() + { + // Compile-time assertion: fails to build if PrepareReturnReqestListModel still exists + // instead of PrepareReturnRequestListModel (spec §2.4 typo fix). + IMerchandiseReturnViewModelService service = null; + Func _ = () => service.PrepareReturnRequestListModel(); + } + + [TestMethod] + public async Task PrepareMerchandiseReturnModel_RegisterCustomersPerStore_ScopesEmailLookupToCurrentStore() + { + var customerServiceMock = new Mock(); + customerServiceMock + .Setup(c => c.GetCustomerByEmail("test@example.com", "store-1")) + .ReturnsAsync(new Customer { Id = "customer-1" }); + + var storeContextMock = new Mock(); + storeContextMock.Setup(s => s.CurrentStore).Returns(new Grand.Domain.Stores.Store { Id = "store-1" }); + var contextAccessorMock = new Mock(); + contextAccessorMock.Setup(c => c.StoreContext).Returns(storeContextMock.Object); + + var customerConfig = new CustomerConfig { RegisterCustomersPerStore = true }; + + var service = Build(customerServiceMock.Object, contextAccessorMock.Object, customerConfig); + + var model = new MerchandiseReturnListModel { SearchCustomerEmail = "Test@Example.com" }; + await service.PrepareMerchandiseReturnModel(model, 1, 10); + + // ToLowerInvariant() is kept (spec §2.4: AdminShared's existing lower-casing survives; + // Vendor's omission of it was the drift, not the other way around). + customerServiceMock.Verify(c => c.GetCustomerByEmail("test@example.com", "store-1"), Times.Once); + } + + [TestMethod] + public async Task PrepareMerchandiseReturnModel_RegisterCustomersPerStoreDisabled_LooksUpAcrossAllStores() + { + var customerServiceMock = new Mock(); + customerServiceMock + .Setup(c => c.GetCustomerByEmail("test@example.com", "")) + .ReturnsAsync(new Customer { Id = "customer-1" }); + + var contextAccessorMock = new Mock(); + var customerConfig = new CustomerConfig { RegisterCustomersPerStore = false }; + + var service = Build(customerServiceMock.Object, contextAccessorMock.Object, customerConfig); + + var model = new MerchandiseReturnListModel { SearchCustomerEmail = "Test@Example.com" }; + await service.PrepareMerchandiseReturnModel(model, 1, 10); + + customerServiceMock.Verify(c => c.GetCustomerByEmail("test@example.com", ""), Times.Once); + } + + [TestMethod] + public async Task PrepareMerchandiseReturnModel_ForwardsScopeDefaultStoreIdAndVendorIdToService() + { + // Regression test for coverage gap noted in Task 4: verify that scope.DefaultStoreId + // and scope.DefaultVendorId actually reach IMerchandiseReturnService.SearchMerchandiseReturns + // with the exact values (not just It.IsAny). + var merchandiseReturnServiceMock = new Mock(); + // Method signature: SearchMerchandiseReturns(storeId, customerId, orderItemId, vendorId, ownerId, rs, pageIndex, pageSize, createdFromUtc, createdToUtc) + merchandiseReturnServiceMock + .Setup(m => m.SearchMerchandiseReturns( + "store-42", It.IsAny(), It.IsAny(), "vendor-99", It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny())) + .ReturnsAsync(new PagedList()); + + var contextAccessorMock = new Mock(); + var scopeMock = new Mock>(); + scopeMock.Setup(s => s.DefaultStoreId).Returns("store-42"); + scopeMock.Setup(s => s.DefaultVendorId).Returns("vendor-99"); + + var service = BuildWithCustomMocks( + merchandiseReturnServiceMock.Object, + contextAccessorMock.Object, + scopeMock.Object); + + var model = new MerchandiseReturnListModel(); + await service.PrepareMerchandiseReturnModel(model, 1, 10); + + // Verify exact call with scope values, not It.IsAny + merchandiseReturnServiceMock.Verify(m => m.SearchMerchandiseReturns( + "store-42", It.IsAny(), It.IsAny(), "vendor-99", It.IsAny(), + It.IsAny(), 0, 10, + It.IsAny(), It.IsAny()), Times.Once); + } + + private static MerchandiseReturnViewModelService Build(ICustomerService customerService, + IContextAccessor contextAccessor, CustomerConfig customerConfig) + { + var orderServiceMock = new Mock(); + var productServiceMock = new Mock(); + var dateTimeServiceMock = new Mock(); + dateTimeServiceMock.Setup(d => d.CurrentTimeZone).Returns(TimeZoneInfo.Utc); + var translationServiceMock = new Mock(); + var messageProviderServiceMock = new Mock(); + var languageSettings = new LanguageSettings(); + var merchandiseReturnServiceMock = new Mock(); + merchandiseReturnServiceMock + .Setup(m => m.SearchMerchandiseReturns( + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny())) + .ReturnsAsync(new PagedList()); + var priceFormatterMock = new Mock(); + var addressSettings = new AddressSettings(); + var countryServiceMock = new Mock(); + var addressAttributeServiceMock = new Mock(); + var addressAttributeParserMock = new Mock(); + var downloadServiceMock = new Mock(); + var orderSettings = new OrderSettings(); + var enumTranslationServiceMock = new Mock(); + var scopeMock = new Mock>(); + scopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); + scopeMock.Setup(s => s.DefaultVendorId).Returns((string)null); + + return new MerchandiseReturnViewModelService( + orderServiceMock.Object, + productServiceMock.Object, + customerService, + dateTimeServiceMock.Object, + translationServiceMock.Object, + messageProviderServiceMock.Object, + languageSettings, + merchandiseReturnServiceMock.Object, + priceFormatterMock.Object, + addressSettings, + countryServiceMock.Object, + addressAttributeServiceMock.Object, + addressAttributeParserMock.Object, + downloadServiceMock.Object, + orderSettings, + enumTranslationServiceMock.Object, + contextAccessor, + customerConfig, + scopeMock.Object); + } + + private static MerchandiseReturnViewModelService BuildWithCustomMocks( + IMerchandiseReturnService merchandiseReturnService, + IContextAccessor contextAccessor, + IAdminDataScope scope) + { + var orderServiceMock = new Mock(); + var productServiceMock = new Mock(); + var customerServiceMock = new Mock(); + var dateTimeServiceMock = new Mock(); + dateTimeServiceMock.Setup(d => d.CurrentTimeZone).Returns(TimeZoneInfo.Utc); + var translationServiceMock = new Mock(); + var messageProviderServiceMock = new Mock(); + var languageSettings = new LanguageSettings(); + var priceFormatterMock = new Mock(); + var addressSettings = new AddressSettings(); + var countryServiceMock = new Mock(); + var addressAttributeServiceMock = new Mock(); + var addressAttributeParserMock = new Mock(); + var downloadServiceMock = new Mock(); + var orderSettings = new OrderSettings(); + var enumTranslationServiceMock = new Mock(); + var customerConfig = new CustomerConfig(); + + return new MerchandiseReturnViewModelService( + orderServiceMock.Object, + productServiceMock.Object, + customerServiceMock.Object, + dateTimeServiceMock.Object, + translationServiceMock.Object, + messageProviderServiceMock.Object, + languageSettings, + merchandiseReturnService, + priceFormatterMock.Object, + addressSettings, + countryServiceMock.Object, + addressAttributeServiceMock.Object, + addressAttributeParserMock.Object, + downloadServiceMock.Object, + orderSettings, + enumTranslationServiceMock.Object, + contextAccessor, + customerConfig, + scope); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/RoutedMerchandiseReturnDataScopeTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/RoutedMerchandiseReturnDataScopeTests.cs new file mode 100644 index 0000000000..d56ab6fadb --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/RoutedMerchandiseReturnDataScopeTests.cs @@ -0,0 +1,82 @@ +using Grand.Domain.Orders; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Admin.Tests.Controllers; + +[TestClass] +public class RoutedMerchandiseReturnDataScopeTests +{ + private static RoutedMerchandiseReturnDataScope Build(string area, + IAdminDataScope admin, IAdminDataScope store, + IAdminDataScope vendor) + { + var httpContext = new DefaultHttpContext(); + if (area != null) + httpContext.Request.RouteValues = new RouteValueDictionary { ["area"] = area }; + var httpContextAccessorMock = new Mock(); + httpContextAccessorMock.Setup(a => a.HttpContext).Returns(httpContext); + return new RoutedMerchandiseReturnDataScope(httpContextAccessorMock.Object, + (GlobalAdminDataScope)admin, (StoreMerchandiseReturnDataScope)store, + (VendorMerchandiseReturnDataScope)vendor); + } + + [TestMethod] + public void AdminArea_ResolvesToGlobalScope() + { + var global = new GlobalAdminDataScope(); + var routed = Build("Admin", global, new StoreMerchandiseReturnDataScope(Mock.Of()), + new VendorMerchandiseReturnDataScope(Mock.Of())); + + Assert.AreEqual("Admin", routed.ResourceKeyPrefix); + Assert.IsNull(routed.DefaultStoreId); + } + + [TestMethod] + public void StoreArea_ResolvesToStoreScope() + { + var workContext = new Mock(); + workContext.Setup(w => w.CurrentCustomer).Returns(new Grand.Domain.Customers.Customer { StaffStoreId = "store-1" }); + var contextAccessorMock = new Mock(); + contextAccessorMock.Setup(c => c.WorkContext).Returns(workContext.Object); + + var routed = Build("Store", new GlobalAdminDataScope(), + new StoreMerchandiseReturnDataScope(contextAccessorMock.Object), + new VendorMerchandiseReturnDataScope(Mock.Of())); + + Assert.AreEqual("store-1", routed.DefaultStoreId); + } + + [TestMethod] + public void VendorArea_ResolvesToVendorScope() + { + var workContext = new Mock(); + workContext.Setup(w => w.CurrentVendor).Returns(new Grand.Domain.Vendors.Vendor { Id = "vendor-A" }); + var contextAccessorMock = new Mock(); + contextAccessorMock.Setup(c => c.WorkContext).Returns(workContext.Object); + + var routed = Build("Vendor", new GlobalAdminDataScope(), + new StoreMerchandiseReturnDataScope(Mock.Of()), + new VendorMerchandiseReturnDataScope(contextAccessorMock.Object)); + + Assert.AreEqual("vendor-A", routed.DefaultVendorId); + } + + [TestMethod] + public void UnrecognizedOrMissingArea_ThrowsFailClosed() + { + var admin = new GlobalAdminDataScope(); + var store = new StoreMerchandiseReturnDataScope(Mock.Of()); + var vendor = new VendorMerchandiseReturnDataScope(Mock.Of()); + + var typo = Build("Vender", admin, store, vendor); + Assert.ThrowsExactly(() => _ = typo.ResourceKeyPrefix); + + var missing = Build(null, admin, store, vendor); + Assert.ThrowsExactly(() => _ = missing.ResourceKeyPrefix); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/StoreMerchandiseReturnDataScopeTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/StoreMerchandiseReturnDataScopeTests.cs new file mode 100644 index 0000000000..383aff61c4 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/StoreMerchandiseReturnDataScopeTests.cs @@ -0,0 +1,64 @@ +using Grand.Domain.Customers; +using Grand.Domain.Orders; +using Grand.Infrastructure; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Services; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Admin.Tests.Controllers; + +[TestClass] +public class StoreMerchandiseReturnDataScopeTests +{ + private static StoreMerchandiseReturnDataScope Build(string staffStoreId) + { + var customer = new Customer { StaffStoreId = staffStoreId }; + var workContextMock = new Mock(); + workContextMock.Setup(w => w.CurrentCustomer).Returns(customer); + var contextAccessorMock = new Mock(); + contextAccessorMock.Setup(c => c.WorkContext).Returns(workContextMock.Object); + return new StoreMerchandiseReturnDataScope(contextAccessorMock.Object); + } + + [TestMethod] + public async Task HasAccess_MatchingStoreId_True() + { + var scope = Build("store-1"); + Assert.IsTrue(await scope.HasAccess(new MerchandiseReturn { StoreId = "store-1" })); + } + + [TestMethod] + public async Task HasAccess_MismatchedStoreId_False() + { + var scope = Build("store-1"); + Assert.IsFalse(await scope.HasAccess(new MerchandiseReturn { StoreId = "store-2" })); + } + + [TestMethod] + public async Task HasAccess_NullEntity_False() + { + var scope = Build("store-1"); + Assert.IsFalse(await scope.HasAccess(null)); + } + + [TestMethod] + public void ScopeDefaults_StoreScoped() + { + var scope = Build("store-1"); + Assert.AreEqual("store-1", scope.DefaultStoreId); + Assert.IsNull(scope.DefaultVendorId); + Assert.AreEqual("Admin", scope.ResourceKeyPrefix); + Assert.IsTrue(scope.ShowStoreSelector); + } + + [TestMethod] + public async Task CanView_NotOverridden_MatchesHasAccess() + { + // No loose/strict split for this entity on Store (spec §2.3) - CanView must fall through to + // the interface default (HasAccess), not be separately implemented. + var scope = Build("store-1"); + var entity = new MerchandiseReturn { StoreId = "store-2" }; + Assert.AreEqual(await scope.HasAccess(entity), await ((IAdminDataScope)scope).CanView(entity)); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/VendorMerchandiseReturnDataScopeTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/VendorMerchandiseReturnDataScopeTests.cs new file mode 100644 index 0000000000..f2c4895cb6 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/VendorMerchandiseReturnDataScopeTests.cs @@ -0,0 +1,53 @@ +using Grand.Domain.Orders; +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 VendorMerchandiseReturnDataScopeTests +{ + private static VendorMerchandiseReturnDataScope 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 VendorMerchandiseReturnDataScope(contextAccessorMock.Object); + } + + [TestMethod] + public async Task HasAccess_MatchingVendorId_True() + { + var scope = Build("vendor-A"); + Assert.IsTrue(await scope.HasAccess(new MerchandiseReturn { VendorId = "vendor-A" })); + } + + [TestMethod] + public async Task HasAccess_MismatchedVendorId_False() + { + var scope = Build("vendor-A"); + Assert.IsFalse(await scope.HasAccess(new MerchandiseReturn { VendorId = "vendor-B" })); + } + + [TestMethod] + public async Task HasAccess_NullEntity_False() + { + var scope = Build("vendor-A"); + Assert.IsFalse(await scope.HasAccess(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/Grand.Web.Admin.Tests.csproj b/src/Tests/Grand.Web.Admin.Tests/Grand.Web.Admin.Tests.csproj index d6b8f64bb9..3ff9189ca9 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Grand.Web.Admin.Tests.csproj +++ b/src/Tests/Grand.Web.Admin.Tests/Grand.Web.Admin.Tests.csproj @@ -21,5 +21,11 @@ + + StoreHost + + + VendorHost + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/CreateOrUpdate.TabAddress.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/CreateOrUpdate.TabAddress.cshtml deleted file mode 100644 index 3ca211e2d3..0000000000 --- a/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/CreateOrUpdate.TabAddress.cshtml +++ /dev/null @@ -1,15 +0,0 @@ -@model MerchandiseReturnModel -
- -
- @if (Model.PickupAddress != null) - { -
- -
- -
- } -
- -
\ No newline at end of file diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/WidgetZone.DetailsButtons.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/WidgetZone.DetailsButtons.cshtml new file mode 100644 index 0000000000..f3f5fcf452 --- /dev/null +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/WidgetZone.DetailsButtons.cshtml @@ -0,0 +1,2 @@ +@model MerchandiseReturnModel + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/WidgetZone.DetailsTabs.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/WidgetZone.DetailsTabs.cshtml new file mode 100644 index 0000000000..775a71d9c6 --- /dev/null +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/WidgetZone.DetailsTabs.cshtml @@ -0,0 +1,2 @@ +@model MerchandiseReturnModel + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/WidgetZone.Documents.Bottom.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/WidgetZone.Documents.Bottom.cshtml new file mode 100644 index 0000000000..f26b9a9d99 --- /dev/null +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/WidgetZone.Documents.Bottom.cshtml @@ -0,0 +1,2 @@ +@model MerchandiseReturnModel + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/WidgetZone.Documents.Top.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/WidgetZone.Documents.Top.cshtml new file mode 100644 index 0000000000..87cd7ba14f --- /dev/null +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/WidgetZone.Documents.Top.cshtml @@ -0,0 +1,2 @@ +@model MerchandiseReturnModel + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/WidgetZone.ListButtons.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/WidgetZone.ListButtons.cshtml new file mode 100644 index 0000000000..7739ab5624 --- /dev/null +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/WidgetZone.ListButtons.cshtml @@ -0,0 +1,2 @@ +@model MerchandiseReturnListModel + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/WidgetZone.Notes.Bottom.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/WidgetZone.Notes.Bottom.cshtml new file mode 100644 index 0000000000..bdbe34f98b --- /dev/null +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/WidgetZone.Notes.Bottom.cshtml @@ -0,0 +1,2 @@ +@model MerchandiseReturnModel + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/WidgetZone.Notes.Top.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/WidgetZone.Notes.Top.cshtml new file mode 100644 index 0000000000..96a192cbc5 --- /dev/null +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/WidgetZone.Notes.Top.cshtml @@ -0,0 +1,2 @@ +@model MerchandiseReturnModel + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/WidgetZone.TabAddress.Bottom.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/WidgetZone.TabAddress.Bottom.cshtml new file mode 100644 index 0000000000..502f92a470 --- /dev/null +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/WidgetZone.TabAddress.Bottom.cshtml @@ -0,0 +1,2 @@ +@model MerchandiseReturnModel + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/WidgetZone.TabAddress.Top.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/WidgetZone.TabAddress.Top.cshtml new file mode 100644 index 0000000000..988515f18a --- /dev/null +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/WidgetZone.TabAddress.Top.cshtml @@ -0,0 +1,2 @@ +@model MerchandiseReturnModel + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/WidgetZone.TabInfo.Bottom.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/WidgetZone.TabInfo.Bottom.cshtml new file mode 100644 index 0000000000..3b7aa1e369 --- /dev/null +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/WidgetZone.TabInfo.Bottom.cshtml @@ -0,0 +1,2 @@ +@model MerchandiseReturnModel + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/WidgetZone.TabInfo.Top.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/WidgetZone.TabInfo.Top.cshtml new file mode 100644 index 0000000000..4ad0d67a5d --- /dev/null +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/WidgetZone.TabInfo.Top.cshtml @@ -0,0 +1,2 @@ +@model MerchandiseReturnModel + diff --git a/src/Web/Grand.Web.Admin/Controllers/MerchandiseReturnController.cs b/src/Web/Grand.Web.Admin/Controllers/MerchandiseReturnController.cs index b4208c09f1..ed250075de 100644 --- a/src/Web/Grand.Web.Admin/Controllers/MerchandiseReturnController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/MerchandiseReturnController.cs @@ -1,231 +1,29 @@ -using Grand.Business.Core.Interfaces.Checkout.Orders; -using Grand.Business.Core.Interfaces.Common.Addresses; +using Grand.Business.Core.Interfaces.Checkout.Orders; using Grand.Business.Core.Interfaces.Common.Localization; -using Grand.Domain.Common; using Grand.Domain.Orders; -using Grand.Domain.Permissions; -using Grand.Web.AdminShared.Extensions; +using Grand.Web.Admin.Extensions; +using Grand.Web.AdminShared.Controllers; using Grand.Web.AdminShared.Interfaces; -using Grand.Web.AdminShared.Models.Orders; -using Grand.Web.Common.DataSource; using Grand.Web.Common.Filters; -using Grand.Web.Common.Security.Authorization; using Microsoft.AspNetCore.Mvc; namespace Grand.Web.Admin.Controllers; -[PermissionAuthorize(PermissionSystemName.MerchandiseReturns)] -public class MerchandiseReturnController : BaseAdminController -{ - #region Constructors - - public MerchandiseReturnController( - IMerchandiseReturnViewModelService merchandiseReturnViewModelService, - ITranslationService translationService, - IMerchandiseReturnService merchandiseReturnService, - IOrderService orderService) - { - _merchandiseReturnViewModelService = merchandiseReturnViewModelService; - _translationService = translationService; - _merchandiseReturnService = merchandiseReturnService; - _orderService = orderService; - } - - #endregion - - #region Fields - - private readonly IMerchandiseReturnViewModelService _merchandiseReturnViewModelService; - private readonly ITranslationService _translationService; - private readonly IMerchandiseReturnService _merchandiseReturnService; - private readonly IOrderService _orderService; - - #endregion Fields - - #region Methods - - //list - public IActionResult Index() - { - return RedirectToAction("List"); - } - - public IActionResult List() - { - var model = _merchandiseReturnViewModelService.PrepareReturnReqestListModel(); - return View(model); - } - - [PermissionAuthorizeAction(PermissionActionName.List)] - [HttpPost] - public async Task List(DataSourceRequest command, MerchandiseReturnListModel model) - { - var merchandiseReturnModels = await _merchandiseReturnViewModelService.PrepareMerchandiseReturnModel(model, command.Page, command.PageSize); - var gridModel = new DataSourceResult { - Data = merchandiseReturnModels.merchandiseReturnModels, - Total = merchandiseReturnModels.totalCount - }; - - return Json(gridModel); - } - - [PermissionAuthorizeAction(PermissionActionName.Preview)] - [HttpPost] - public async Task GoToId(MerchandiseReturnListModel model) - { - if (model.GoDirectlyToId == null) - return RedirectToAction("List", "MerchandiseReturn"); - - int.TryParse(model.GoDirectlyToId, out var id); - - //try to load a product entity - var merchandiseReturn = await _merchandiseReturnService.GetMerchandiseReturnById(id); - if (merchandiseReturn == null) - //not found - return RedirectToAction("List", "MerchandiseReturn"); - - return RedirectToAction("Edit", "MerchandiseReturn", new { id = merchandiseReturn.Id }); - } - - [PermissionAuthorizeAction(PermissionActionName.Preview)] - [HttpPost] - public async Task ProductsForMerchandiseReturn(string merchandiseReturnId, DataSourceRequest command) - { - var merchandiseReturn = await _merchandiseReturnService.GetMerchandiseReturnById(merchandiseReturnId); - if (merchandiseReturn == null) - return ErrorForKendoGridJson("Merchandise return not found"); - - var items = await _merchandiseReturnViewModelService.PrepareMerchandiseReturnItemModel(merchandiseReturnId); - var gridModel = new DataSourceResult { - Data = items, - Total = items.Count - }; - - return Json(gridModel); - } - - //edit - [PermissionAuthorizeAction(PermissionActionName.Preview)] - public async Task Edit(string id) - { - var merchandiseReturn = await _merchandiseReturnService.GetMerchandiseReturnById(id); - if (merchandiseReturn == null) - //No merchandise return found with the specified id - return RedirectToAction("List"); - - var model = new MerchandiseReturnModel(); - await _merchandiseReturnViewModelService.PrepareMerchandiseReturnModel(model, merchandiseReturn, false); - return View(model); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - [HttpPost] - [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")] - public async Task Edit(MerchandiseReturnModel model, bool continueEditing, - [FromServices] IAddressAttributeService addressAttributeService, - [FromServices] IAddressAttributeParser addressAttributeParser, - [FromServices] OrderSettings orderSettings - ) - { - var merchandiseReturn = await _merchandiseReturnService.GetMerchandiseReturnById(model.Id); - if (merchandiseReturn == null) - //No merchandise return found with the specified id - return RedirectToAction("List"); - - if (ModelState.IsValid) - { - var customAddressAttributes = new List(); - if (orderSettings.MerchandiseReturns_AllowToSpecifyPickupAddress) - customAddressAttributes = - await model.PickupAddress.ParseCustomAddressAttributes(addressAttributeParser, - addressAttributeService); - merchandiseReturn = - await _merchandiseReturnViewModelService.UpdateMerchandiseReturnModel(merchandiseReturn, model, - customAddressAttributes); - - Success(_translationService.GetResource("Admin.Orders.MerchandiseReturns.Updated")); - return continueEditing - ? RedirectToAction("Edit", new { id = merchandiseReturn.Id }) - : RedirectToAction("List"); - } - - //If we got this far, something failed, redisplay form - await _merchandiseReturnViewModelService.PrepareMerchandiseReturnModel(model, merchandiseReturn, false); - return View(model); - } - - //delete - [PermissionAuthorizeAction(PermissionActionName.Delete)] - [HttpPost] - public async Task Delete(string id) - { - var merchandiseReturn = await _merchandiseReturnService.GetMerchandiseReturnById(id); - if (merchandiseReturn == null) - //No merchandise return found with the specified id - return RedirectToAction("List"); - - if (ModelState.IsValid) - { - await _merchandiseReturnViewModelService.DeleteMerchandiseReturn(merchandiseReturn); - Success(_translationService.GetResource("Admin.Orders.MerchandiseReturns.Deleted")); - return RedirectToAction("List"); - } - - Error(ModelState); - return RedirectToAction("Edit", new { id = merchandiseReturn.Id }); - } - - #endregion - - #region Merchandise return notes - - [PermissionAuthorizeAction(PermissionActionName.Preview)] - [HttpPost] - public async Task MerchandiseReturnNotesSelect(string merchandiseReturnId, DataSourceRequest command) - { - var merchandiseReturn = await _merchandiseReturnService.GetMerchandiseReturnById(merchandiseReturnId); - if (merchandiseReturn == null) - throw new ArgumentException("No merchandise return found with the specified id"); - - //merchandise return notes - var merchandiseReturnNoteModels = await _merchandiseReturnViewModelService.PrepareMerchandiseReturnNotes(merchandiseReturn); - var gridModel = new DataSourceResult { - Data = merchandiseReturnNoteModels, - Total = merchandiseReturnNoteModels.Count - }; - return Json(gridModel); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - public async Task MerchandiseReturnNoteAdd(string merchandiseReturnId, string orderId, - string downloadId, bool displayToCustomer, string message) - { - var merchandiseReturn = await _merchandiseReturnService.GetMerchandiseReturnById(merchandiseReturnId); - if (merchandiseReturn == null) - return Json(new { Result = false }); - - var order = await _orderService.GetOrderById(orderId); - if (order == null) - return Json(new { Result = false }); - - await _merchandiseReturnViewModelService.InsertMerchandiseReturnNote(merchandiseReturn, order, downloadId, - displayToCustomer, message); - - return Json(new { Result = true }); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - [HttpPost] - public async Task MerchandiseReturnNoteDelete(string id, string merchandiseReturnId) - { - var merchandiseReturn = await _merchandiseReturnService.GetMerchandiseReturnById(merchandiseReturnId); - if (merchandiseReturn == null) - throw new ArgumentException("No merchandise return found with the specified id"); - - await _merchandiseReturnViewModelService.DeleteMerchandiseReturnNote(merchandiseReturn, id); - - return new JsonResult(""); - } - - #endregion -} \ No newline at end of file +// Concrete host subclass of BaseMerchandiseReturnController (ARCH-001 MerchandiseReturn +// consolidation). This class supplies Admin's DI wiring plus the attributes that used to arrive +// transitively via BaseAdminController - BaseMerchandiseReturnController can't inherit any single +// host's base controller (it's shared across Admin/Store/Vendor, each with a different +// [Area]/[Authorize*] pair), so each subclass restates its own host's attribute set explicitly, same +// pattern as OrderController. +[AuthorizeAdmin] +[AutoValidateAntiforgeryToken] +[Area(Constants.AreaAdmin)] +[AuthorizeMenu] +public class MerchandiseReturnController( + IMerchandiseReturnViewModelService merchandiseReturnViewModelService, + ITranslationService translationService, + IMerchandiseReturnService merchandiseReturnService, + IOrderService orderService, + IAdminDataScope scope) + : BaseMerchandiseReturnController(merchandiseReturnViewModelService, translationService, + merchandiseReturnService, orderService, scope); diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseMerchandiseReturnController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseMerchandiseReturnController.cs new file mode 100644 index 0000000000..a82e6a4fc9 --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseMerchandiseReturnController.cs @@ -0,0 +1,234 @@ +using Grand.Business.Core.Interfaces.Checkout.Orders; +using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Domain.Orders; +using Grand.Domain.Permissions; +using Grand.Web.AdminShared.Extensions; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Orders; +using Grand.Web.Common.Controllers; +using Grand.Web.Common.DataSource; +using Grand.Web.Common.Security.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Grand.Web.AdminShared.Controllers; + +[PermissionAuthorize(PermissionSystemName.MerchandiseReturns)] +[AutoValidateAntiforgeryToken] +public abstract class BaseMerchandiseReturnController( + IMerchandiseReturnViewModelService merchandiseReturnViewModelService, + ITranslationService translationService, + IMerchandiseReturnService merchandiseReturnService, + IOrderService orderService, + IAdminDataScope scope) + : BaseController +{ + // Exposed for host subclasses: primary-constructor parameters aren't visible to derived classes + // by name in C#. + protected IMerchandiseReturnViewModelService MerchandiseReturnViewModelService => merchandiseReturnViewModelService; + protected ITranslationService TranslationService => translationService; + protected IMerchandiseReturnService MerchandiseReturnService => merchandiseReturnService; + protected IOrderService OrderService => orderService; + protected IAdminDataScope Scope => scope; + + #region List + + public IActionResult Index() => RedirectToAction("List"); + + public IActionResult List() + { + var model = merchandiseReturnViewModelService.PrepareReturnRequestListModel(); + return View(model); + } + + [PermissionAuthorizeAction(PermissionActionName.List)] + [HttpPost] + public async Task List(DataSourceRequest command, MerchandiseReturnListModel model) + { + // Vendor needs no analogous model.StoreId forcing here (no store concept) - its + // vendor-scoping happens inside the shared service call via scope.DefaultVendorId (Task 4). + if (scope.DefaultStoreId is not null) model.StoreId = scope.DefaultStoreId; + + var (merchandiseReturnModels, totalCount) = + await merchandiseReturnViewModelService.PrepareMerchandiseReturnModel(model, command.Page, command.PageSize); + + var gridModel = new DataSourceResult { + Data = merchandiseReturnModels, + Total = totalCount + }; + return Json(gridModel); + } + + #endregion + + #region GoToId / Products + + [PermissionAuthorizeAction(PermissionActionName.Preview)] + [HttpPost] + public async Task GoToId(MerchandiseReturnListModel model) + { + if (model.GoDirectlyToId == null) + return RedirectToAction("List"); + + int.TryParse(model.GoDirectlyToId, out var id); + + var merchandiseReturn = await MerchandiseReturnService.GetMerchandiseReturnById(id); + if (merchandiseReturn == null || !await Scope.HasAccess(merchandiseReturn)) + return RedirectToAction("List"); + + return RedirectToAction("Edit", new { id = merchandiseReturn.Id }); + } + + [PermissionAuthorizeAction(PermissionActionName.Preview)] + [HttpPost] + public async Task ProductsForMerchandiseReturn(string merchandiseReturnId, DataSourceRequest command) + { + var merchandiseReturn = await MerchandiseReturnService.GetMerchandiseReturnById(merchandiseReturnId); + if (merchandiseReturn == null || !await Scope.HasAccess(merchandiseReturn)) + return ErrorForKendoGridJson("Merchandise return not found"); + + var items = await MerchandiseReturnViewModelService.PrepareMerchandiseReturnItemModel(merchandiseReturnId); + var gridModel = new DataSourceResult { + Data = items, + Total = items.Count + }; + return Json(gridModel); + } + + #endregion + + #region Edit + + [PermissionAuthorizeAction(PermissionActionName.Preview)] + public async Task Edit(string id) + { + var merchandiseReturn = await MerchandiseReturnService.GetMerchandiseReturnById(id); + if (merchandiseReturn == null) return RedirectToAction("List"); + if (!await Scope.CanView(merchandiseReturn)) return RedirectToAction("List"); + + var model = new MerchandiseReturnModel(); + await MerchandiseReturnViewModelService.PrepareMerchandiseReturnModel(model, merchandiseReturn, false); + return View(model); + } + + [PermissionAuthorizeAction(PermissionActionName.Edit)] + [HttpPost] + [Grand.Web.Common.Filters.ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")] + public async Task Edit(MerchandiseReturnModel model, bool continueEditing, + [FromServices] Grand.Business.Core.Interfaces.Common.Addresses.IAddressAttributeService addressAttributeService, + [FromServices] Grand.Business.Core.Interfaces.Common.Addresses.IAddressAttributeParser addressAttributeParser, + [FromServices] Grand.Domain.Orders.OrderSettings orderSettings) + { + var merchandiseReturn = await MerchandiseReturnService.GetMerchandiseReturnById(model.Id); + if (merchandiseReturn == null) return RedirectToAction("List"); + if (!await Scope.HasAccess(merchandiseReturn)) return RedirectToAction("List"); + + if (ModelState.IsValid) + { + var customAddressAttributes = new List(); + if (orderSettings.MerchandiseReturns_AllowToSpecifyPickupAddress) + customAddressAttributes = await model.PickupAddress.ParseCustomAddressAttributes( + addressAttributeParser, addressAttributeService); + + merchandiseReturn = await MerchandiseReturnViewModelService.UpdateMerchandiseReturnModel( + merchandiseReturn, model, customAddressAttributes); + + Success(TranslationService.GetResource($"{Scope.ResourceKeyPrefix}.Orders.MerchandiseReturns.Updated")); + return continueEditing + ? RedirectToAction("Edit", new { id = merchandiseReturn.Id }) + : RedirectToAction("List"); + } + + await MerchandiseReturnViewModelService.PrepareMerchandiseReturnModel(model, merchandiseReturn, false); + return View(model); + } + + #endregion + + #region Delete + + [PermissionAuthorizeAction(PermissionActionName.Delete)] + [HttpPost] + public async Task Delete(string id) + { + var merchandiseReturn = await merchandiseReturnService.GetMerchandiseReturnById(id); + if (merchandiseReturn == null) return RedirectToAction("List"); + if (!await scope.HasAccess(merchandiseReturn)) return RedirectToAction("List"); + + if (ModelState.IsValid) + { + await merchandiseReturnViewModelService.DeleteMerchandiseReturn(merchandiseReturn); + Success(translationService.GetResource($"{scope.ResourceKeyPrefix}.Orders.MerchandiseReturns.Deleted")); + } + + return RedirectToAction("List"); + } + + #endregion + + #region Merchandise return notes + + /// Deliberately preserved host divergence (spec §5/§11, DECIDED — not unified): Admin's + /// and Vendor's original controllers both throw ArgumentException for a not-found-or-denied + /// merchandise return; Store's soft-denies with an empty Content("") response instead, because + /// Store's Kendo grid error path expects a 200 with an empty body, not a 500. This seam is + /// written from scratch for this phase - it is NOT copied from BaseOrderManagementController's + /// OrderNotesSelect, which (as shipped) has no host-differentiated response at all (see this + /// plan's Global Constraints for the full explanation). Default here matches Admin/Vendor; only + /// Grand.Web.Store's concrete MerchandiseReturnController overrides this (Task 10). + protected virtual IActionResult NotFoundOrDeniedForNotesSelect() => + throw new ArgumentException("No merchandise return found with the specified id"); + + [PermissionAuthorizeAction(PermissionActionName.Preview)] + [HttpPost] + public async Task MerchandiseReturnNotesSelect(string merchandiseReturnId, DataSourceRequest command) + { + var merchandiseReturn = await merchandiseReturnService.GetMerchandiseReturnById(merchandiseReturnId); + if (merchandiseReturn == null || !await scope.HasAccess(merchandiseReturn)) + return NotFoundOrDeniedForNotesSelect(); + + var merchandiseReturnNoteModels = await merchandiseReturnViewModelService.PrepareMerchandiseReturnNotes(merchandiseReturn); + var gridModel = new DataSourceResult { + Data = merchandiseReturnNoteModels, + Total = merchandiseReturnNoteModels.Count + }; + return Json(gridModel); + } + + [PermissionAuthorizeAction(PermissionActionName.Edit)] + public async Task MerchandiseReturnNoteAdd(string merchandiseReturnId, string downloadId, + bool displayToCustomer, string message) + { + var merchandiseReturn = await merchandiseReturnService.GetMerchandiseReturnById(merchandiseReturnId); + if (merchandiseReturn == null || !await scope.HasAccess(merchandiseReturn)) + return Json(new { Result = false }); + + // Spec §2.5/§11 (approved): no caller-supplied orderId - always resolve the entity's own + // order server-side. Closes Admin's original gap (never validated a caller-supplied orderId + // matched the merchandise return), makes Store's now-redundant equality check unnecessary, + // and needs no new parameter on Vendor's side. + var order = await orderService.GetOrderById(merchandiseReturn.OrderId); + if (order == null) return Json(new { Result = false }); + + await merchandiseReturnViewModelService.InsertMerchandiseReturnNote(merchandiseReturn, order, downloadId, + displayToCustomer, message); + + return Json(new { Result = true }); + } + + [PermissionAuthorizeAction(PermissionActionName.Edit)] + [HttpPost] + public async Task MerchandiseReturnNoteDelete(string id, string merchandiseReturnId) + { + var merchandiseReturn = await merchandiseReturnService.GetMerchandiseReturnById(merchandiseReturnId); + if (merchandiseReturn == null) + throw new ArgumentException("No merchandise return found with the specified id"); + if (!await scope.HasAccess(merchandiseReturn)) + return Json(new { Result = false }); + + await merchandiseReturnViewModelService.DeleteMerchandiseReturnNote(merchandiseReturn, id); + + return new JsonResult(""); + } + + #endregion +} diff --git a/src/Web/Grand.Web.AdminShared/Interfaces/IMerchandiseReturnViewModelService.cs b/src/Web/Grand.Web.AdminShared/Interfaces/IMerchandiseReturnViewModelService.cs index 99a4dc58f5..1cea8e5330 100644 --- a/src/Web/Grand.Web.AdminShared/Interfaces/IMerchandiseReturnViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Interfaces/IMerchandiseReturnViewModelService.cs @@ -15,7 +15,7 @@ Task PrepareMerchandiseReturnModel(MerchandiseReturnMode Task PrepareAddressModel(AddressModel model, Address address, bool excludeProperties); Task NotifyCustomer(MerchandiseReturn merchandiseReturn); - MerchandiseReturnListModel PrepareReturnReqestListModel(); + MerchandiseReturnListModel PrepareReturnRequestListModel(); Task> PrepareMerchandiseReturnItemModel( string merchandiseReturnId); diff --git a/src/Web/Grand.Web.AdminShared/Services/MerchandiseReturnViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/MerchandiseReturnViewModelService.cs index e73f88b2a5..351deae882 100644 --- a/src/Web/Grand.Web.AdminShared/Services/MerchandiseReturnViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/MerchandiseReturnViewModelService.cs @@ -11,6 +11,8 @@ using Grand.Domain.Directory; using Grand.Domain.Localization; using Grand.Domain.Orders; +using Grand.Infrastructure; +using Grand.Infrastructure.Configuration; using Grand.Web.AdminShared.Extensions.Mapping; using Grand.Web.AdminShared.Interfaces; using Grand.Web.AdminShared.Models.Common; @@ -36,7 +38,10 @@ public class MerchandiseReturnViewModelService( IAddressAttributeParser addressAttributeParser, IDownloadService downloadService, OrderSettings orderSettings, - IEnumTranslationService enumTranslationService) + IEnumTranslationService enumTranslationService, + IContextAccessor contextAccessor, + CustomerConfig customerConfig, + IAdminDataScope scope) : IMerchandiseReturnViewModelService { public virtual async Task PrepareMerchandiseReturnModel(MerchandiseReturnModel model, @@ -94,7 +99,14 @@ public virtual async Task PrepareMerchandiseReturnModel( var customerId = string.Empty; if (!string.IsNullOrEmpty(model.SearchCustomerEmail)) { - var customer = await customerService.GetCustomerByEmail(model.SearchCustomerEmail.ToLowerInvariant()); + //with per-store customer identity the same e-mail may exist in several stores - scope + //the lookup to the current store so the search matches this store's customer (folded in + //from Vendor's original service, spec §2.4/§11 - approved, disclosed behavior change for + //Admin/Store when CustomerConfig.RegisterCustomersPerStore is on) + var storeId = customerConfig.RegisterCustomersPerStore + ? contextAccessor.StoreContext.CurrentStore.Id + : ""; + var customer = await customerService.GetCustomerByEmail(model.SearchCustomerEmail.ToLowerInvariant(), storeId); customerId = customer != null ? customer.Id : "00000000-0000-0000-0000-000000000000"; } @@ -106,18 +118,17 @@ public virtual async Task PrepareMerchandiseReturnModel( ? null : dateTimeService.ConvertToUtcTime(model.EndDate.Value, dateTimeService.CurrentTimeZone); - var merchandiseReturns = await merchandiseReturnService.SearchMerchandiseReturns(model.StoreId, - customerId, - "", - "", - "", - model.SearchMerchandiseReturnStatusId >= 0 + var merchandiseReturns = await merchandiseReturnService.SearchMerchandiseReturns( + storeId: scope.DefaultStoreId ?? "", + customerId: customerId, + vendorId: scope.DefaultVendorId ?? "", + rs: model.SearchMerchandiseReturnStatusId >= 0 ? (MerchandiseReturnStatus?)model.SearchMerchandiseReturnStatusId : null, - pageIndex - 1, - pageSize, - startDateValue, - endDateValue); + pageIndex: pageIndex - 1, + pageSize: pageSize, + createdFromUtc: startDateValue, + createdToUtc: endDateValue); var merchandiseReturnModels = new List(); foreach (var rr in merchandiseReturns) { @@ -191,7 +202,7 @@ await messageProviderService.SendMerchandiseReturnStatusChangedCustomerMessage(m languageSettings.DefaultAdminLanguageId); } - public virtual MerchandiseReturnListModel PrepareReturnReqestListModel() + public virtual MerchandiseReturnListModel PrepareReturnRequestListModel() { var model = new MerchandiseReturnListModel { //Merchandise return status diff --git a/src/Web/Grand.Web.AdminShared/Services/RoutedMerchandiseReturnDataScope.cs b/src/Web/Grand.Web.AdminShared/Services/RoutedMerchandiseReturnDataScope.cs new file mode 100644 index 0000000000..ba0af43045 --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Services/RoutedMerchandiseReturnDataScope.cs @@ -0,0 +1,48 @@ +#nullable enable + +using Grand.Domain.Orders; +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 /: +/// Grand.Web (the combined host) ProjectReferences Admin, Store, and Vendor +/// directly and loads all three StartupApplications into one DI container, so a plain +/// per-host registration would let whichever host's StartupApplication ran last win for every +/// area in that process. +/// +public class RoutedMerchandiseReturnDataScope( + IHttpContextAccessor httpContextAccessor, + GlobalAdminDataScope adminScope, + StoreMerchandiseReturnDataScope storeScope, + VendorMerchandiseReturnDataScope vendorScope) : IAdminDataScope +{ + private IAdminDataScope Resolved + { + get + { + var area = httpContextAccessor.HttpContext?.Request.RouteValues["area"] as string; + return area switch { + "Admin" => adminScope, + "Store" => storeScope, + "Vendor" => vendorScope, + //fail closed: this object fronts store/vendor tenant isolation, so an unrecognized + //or missing area must never silently resolve to any concrete scope + _ => throw new InvalidOperationException( + $"RoutedMerchandiseReturnDataScope: unrecognized or missing area '{area}'.") + }; + } + } + + public Task HasAccess(MerchandiseReturn entity) => Resolved.HasAccess(entity); + public Task CanView(MerchandiseReturn 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/StoreMerchandiseReturnDataScope.cs b/src/Web/Grand.Web.AdminShared/Services/StoreMerchandiseReturnDataScope.cs new file mode 100644 index 0000000000..69ec2c8f67 --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Services/StoreMerchandiseReturnDataScope.cs @@ -0,0 +1,32 @@ +#nullable enable + +using Grand.Domain.Orders; +using Grand.Infrastructure; +using Grand.Web.AdminShared.Interfaces; + +namespace Grand.Web.AdminShared.Services; + +/// +/// Store's . Bespoke, not the generic +/// : MerchandiseReturn is a plain +/// with a single StoreId field, not +/// IStoreLinkEntity, so the generic class's where TEntity : BaseEntity, +/// IStoreLinkEntity constraint doesn't apply. Mirrors Store's original controller's +/// merchandiseReturn.StoreId != StaffStoreId check, repeated at every action site in that +/// file. No override: Store's original code has +/// one uniform check for both viewing and mutating, no loose/strict split (spec §2.3) — CanView +/// is simply inherited from the interface default, which delegates to . +/// +public class StoreMerchandiseReturnDataScope(IContextAccessor contextAccessor) + : IAdminDataScope +{ + public Task HasAccess(MerchandiseReturn entity) => + Task.FromResult(entity is not null && + entity.StoreId == contextAccessor.WorkContext.CurrentCustomer.StaffStoreId); + + public string? DefaultStoreId => contextAccessor.WorkContext.CurrentCustomer.StaffStoreId; + public string ResourceKeyPrefix => "Admin"; + public bool ShowStoreSelector => true; + public string? DefaultVendorId => null; + public bool CanFeatureOnHomepage => true; // unused for this entity; required interface member +} diff --git a/src/Web/Grand.Web.AdminShared/Services/VendorMerchandiseReturnDataScope.cs b/src/Web/Grand.Web.AdminShared/Services/VendorMerchandiseReturnDataScope.cs new file mode 100644 index 0000000000..1d76d4b80b --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Services/VendorMerchandiseReturnDataScope.cs @@ -0,0 +1,32 @@ +#nullable enable + +using Grand.Domain.Orders; +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 — simpler than Order's child-collection +/// ownership, closer to Shipment's shape. Reimplements the equivalent of +/// Grand.Web.Vendor/Extensions/HasAccess.cs's HasAccessToMerchandiseReturn rather +/// than calling it: Grand.Web.AdminShared has no project reference to +/// Grand.Web.Vendor (the reference direction is Vendor→AdminShared), the same constraint +/// / already work +/// around. Once every controller call site is migrated onto scope.HasAccess, +/// HasAccessToMerchandiseReturn has no remaining callers and is deleted (Task 9). +/// +public class VendorMerchandiseReturnDataScope(IContextAccessor contextAccessor) + : IAdminDataScope +{ + public Task HasAccess(MerchandiseReturn entity) => + Task.FromResult(entity is not null && + 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/Startup/StartupApplication.cs b/src/Web/Grand.Web.AdminShared/Startup/StartupApplication.cs index da6afbdde9..8eaf3fdc9b 100644 --- a/src/Web/Grand.Web.AdminShared/Startup/StartupApplication.cs +++ b/src/Web/Grand.Web.AdminShared/Startup/StartupApplication.cs @@ -108,6 +108,13 @@ public void ConfigureServices(IServiceCollection services, IConfiguration config services.AddScoped>(); services.AddScoped(); services.AddScoped, RoutedPaymentTransactionDataScope>(); + + // IAdminDataScope: Admin reuses the generic GlobalAdminDataScope directly (no + // bespoke Admin scope - confirmed no restriction exists despite the entity's SeId field, spec §2.1). + services.AddScoped>(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped, RoutedMerchandiseReturnDataScope>(); } public void Configure(WebApplication application, IWebHostEnvironment webHostEnvironment) diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Edit.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/MerchandiseReturn/Edit.cshtml similarity index 86% rename from src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Edit.cshtml rename to src/Web/Grand.Web.AdminShared/Views/AdminShared/MerchandiseReturn/Edit.cshtml index f1bdb30b46..b5c55dc1af 100644 --- a/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Edit.cshtml +++ b/src/Web/Grand.Web.AdminShared/Views/AdminShared/MerchandiseReturn/Edit.cshtml @@ -1,9 +1,10 @@ -@model MerchandiseReturnModel +@model MerchandiseReturnModel @{ //page title ViewBag.Title = Loc["Admin.Orders.MerchandiseReturns.EditMerchandiseReturnDetails"]; + var area = ViewContext.RouteData.Values["area"]?.ToString(); } -
+
@@ -27,7 +28,7 @@ @Loc["Admin.Common.Delete"] - +
@@ -38,4 +39,4 @@
- \ No newline at end of file + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/List.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/MerchandiseReturn/List.cshtml similarity index 95% rename from src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/List.cshtml rename to src/Web/Grand.Web.AdminShared/Views/AdminShared/MerchandiseReturn/List.cshtml index febcfec5e9..d6f618b9c5 100644 --- a/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/List.cshtml +++ b/src/Web/Grand.Web.AdminShared/Views/AdminShared/MerchandiseReturn/List.cshtml @@ -1,10 +1,11 @@ -@model MerchandiseReturnListModel +@model MerchandiseReturnListModel @inject AdminAreaSettings adminAreaSettings @{ //page title ViewBag.Title = Loc["Admin.Orders.MerchandiseReturns"]; + var area = ViewContext.RouteData.Values["area"]?.ToString(); } -
+
@@ -14,7 +15,7 @@ @Loc["Admin.Orders.MerchandiseReturns"]
- +
@@ -98,7 +99,7 @@ dataSource: { transport: { read: { - url: "@Html.Raw(Url.Action("List", "MerchandiseReturn", new { area = Constants.AreaAdmin }))", + url: "@Html.Raw(Url.Action("List", "MerchandiseReturn", new { area = area }))", type: "POST", dataType: "json", data: additionalData @@ -148,12 +149,12 @@ field: "CustomerId", title: "@Loc["Admin.Orders.MerchandiseReturns.Fields.Customer"]", width: 200, - template: '#:kendo.htmlEncode(CustomerInfo)#' + template: '#:kendo.htmlEncode(CustomerInfo)#' }, { field: "OrderId", title: "@Loc["Admin.Orders.MerchandiseReturns.Fields.Order"]", width: 80, - template: '#:OrderNumber#' + template: '#:OrderNumber#' }, { field: "MerchandiseReturnStatusStr", title: "@Loc["Admin.Orders.MerchandiseReturns.Fields.Status"]", @@ -204,4 +205,4 @@ } }); }); - \ No newline at end of file + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/CreateOrUpdate.TabAddress.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/MerchandiseReturn/Partials/CreateOrUpdate.TabAddress.cshtml similarity index 59% rename from src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/CreateOrUpdate.TabAddress.cshtml rename to src/Web/Grand.Web.AdminShared/Views/AdminShared/MerchandiseReturn/Partials/CreateOrUpdate.TabAddress.cshtml index 3ca211e2d3..77def73c09 100644 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/CreateOrUpdate.TabAddress.cshtml +++ b/src/Web/Grand.Web.AdminShared/Views/AdminShared/MerchandiseReturn/Partials/CreateOrUpdate.TabAddress.cshtml @@ -1,6 +1,6 @@ -@model MerchandiseReturnModel +@model MerchandiseReturnModel
- +
@if (Model.PickupAddress != null) { @@ -11,5 +11,5 @@
}
- -
\ No newline at end of file + +
diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/CreateOrUpdate.TabDocuments.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/MerchandiseReturn/Partials/CreateOrUpdate.TabDocuments.cshtml similarity index 80% rename from src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/CreateOrUpdate.TabDocuments.cshtml rename to src/Web/Grand.Web.AdminShared/Views/AdminShared/MerchandiseReturn/Partials/CreateOrUpdate.TabDocuments.cshtml index 4ee2e618ee..8a9001e1ee 100644 --- a/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/CreateOrUpdate.TabDocuments.cshtml +++ b/src/Web/Grand.Web.AdminShared/Views/AdminShared/MerchandiseReturn/Partials/CreateOrUpdate.TabDocuments.cshtml @@ -1,14 +1,17 @@ -@model MerchandiseReturnModel +@model MerchandiseReturnModel @inject AdminAreaSettings adminAreaSettings +@{ + var area = ViewContext.RouteData.Values["area"]?.ToString(); +}
- +
- +
\ No newline at end of file + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/CreateOrUpdate.TabInfo.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/MerchandiseReturn/Partials/CreateOrUpdate.TabInfo.cshtml similarity index 91% rename from src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/CreateOrUpdate.TabInfo.cshtml rename to src/Web/Grand.Web.AdminShared/Views/AdminShared/MerchandiseReturn/Partials/CreateOrUpdate.TabInfo.cshtml index 123394514b..955b8100b8 100644 --- a/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/CreateOrUpdate.TabInfo.cshtml +++ b/src/Web/Grand.Web.AdminShared/Views/AdminShared/MerchandiseReturn/Partials/CreateOrUpdate.TabInfo.cshtml @@ -1,9 +1,13 @@ -@model MerchandiseReturnModel +@using Grand.Domain.Orders +@model MerchandiseReturnModel @inject AdminAreaSettings adminAreaSettings @inject OrderSettings orderSettings +@{ + var area = ViewContext.RouteData.Values["area"]?.ToString(); +}
- +
@@ -79,7 +83,7 @@
- +
@@ -92,7 +96,7 @@ dataSource: { transport: { read: { - url: "@Html.Raw(Url.Action("ProductsForMerchandiseReturn", "MerchandiseReturn", new { area = Constants.AreaAdmin }))", + url: "@Html.Raw(Url.Action("ProductsForMerchandiseReturn", "MerchandiseReturn", new { area = area }))", type: "POST", dataType: "json", data: data @@ -123,12 +127,12 @@ field: "ProductName", title: "@Loc["Admin.Orders.MerchandiseReturns.Fields.Item"]", width: 200, - template: '#=kendo.htmlEncode(ProductName)#' + template: '#=kendo.htmlEncode(ProductName)#' }, { field: "ProductSku", title: "@Loc["Admin.Catalog.Products.Fields.Sku"]", width: 200, - template: '#=kendo.htmlEncode(ProductSku)#' + template: '#=kendo.htmlEncode(ProductSku)#' }, { field: "Quantity", title: "@Loc["Admin.Orders.MerchandiseReturns.Fields.Quantity"]", @@ -149,4 +153,4 @@ ] }); }); - \ No newline at end of file + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/CreateOrUpdate.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/MerchandiseReturn/Partials/CreateOrUpdate.cshtml similarity index 92% rename from src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/CreateOrUpdate.cshtml rename to src/Web/Grand.Web.AdminShared/Views/AdminShared/MerchandiseReturn/Partials/CreateOrUpdate.cshtml index 0efc911eaf..05803a9f47 100644 --- a/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/CreateOrUpdate.cshtml +++ b/src/Web/Grand.Web.AdminShared/Views/AdminShared/MerchandiseReturn/Partials/CreateOrUpdate.cshtml @@ -1,4 +1,5 @@ -@using Grand.Business.Core.Interfaces.Common.Security +@using Grand.Business.Core.Interfaces.Common.Security +@using Grand.Domain.Orders @using Grand.Domain.Permissions @model MerchandiseReturnModel @inject OrderSettings orderSettings @@ -54,6 +55,6 @@ - + - \ No newline at end of file + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/MerchandiseReturnNotes.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/MerchandiseReturn/Partials/MerchandiseReturnNotes.cshtml similarity index 92% rename from src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/MerchandiseReturnNotes.cshtml rename to src/Web/Grand.Web.AdminShared/Views/AdminShared/MerchandiseReturn/Partials/MerchandiseReturnNotes.cshtml index e48a64ff4f..335370b74d 100644 --- a/src/Web/Grand.Web.Admin/Areas/Admin/Views/MerchandiseReturn/Partials/MerchandiseReturnNotes.cshtml +++ b/src/Web/Grand.Web.AdminShared/Views/AdminShared/MerchandiseReturn/Partials/MerchandiseReturnNotes.cshtml @@ -1,16 +1,17 @@ -@using Grand.Domain.Media +@using Grand.Domain.Media @model MerchandiseReturnModel @{ ViewData["DownloadType"] = DownloadType.MerchandiseReturn; ViewData["ReferenceId"] = Model.Id; + var area = ViewContext.RouteData.Values["area"]?.ToString(); }
- +
- +
\ No newline at end of file + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Edit.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Edit.cshtml deleted file mode 100644 index 6d6cb2ee9f..0000000000 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Edit.cshtml +++ /dev/null @@ -1,41 +0,0 @@ -@model MerchandiseReturnModel -@{ - //page title - ViewBag.Title = Loc["Admin.Orders.MerchandiseReturns.EditMerchandiseReturnDetails"]; -} - - -
-
-
-
-
- - @Loc["Admin.Orders.MerchandiseReturns.EditMerchandiseReturnDetails"] - - @Html.ActionLink(Loc["Admin.Orders.MerchandiseReturns.BackToList"], "List") - -
-
-
- - - - @Loc["Admin.Common.Delete"] - - -
-
-
-
- -
-
-
-
- - \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/List.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/List.cshtml deleted file mode 100644 index faeec0e56f..0000000000 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/List.cshtml +++ /dev/null @@ -1,207 +0,0 @@ -@model MerchandiseReturnListModel -@inject AdminAreaSettings adminAreaSettings -@{ - //page title - ViewBag.Title = Loc["Admin.Orders.MerchandiseReturns"]; -} -
- -
-
- -
-
-
- - - \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/CreateOrUpdate.TabDocuments.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/CreateOrUpdate.TabDocuments.cshtml deleted file mode 100644 index aadc4a57af..0000000000 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/CreateOrUpdate.TabDocuments.cshtml +++ /dev/null @@ -1,70 +0,0 @@ -@model MerchandiseReturnModel -@inject AdminAreaSettings adminAreaSettings -
- -
-
-
- - -
- - \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/CreateOrUpdate.TabInfo.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/CreateOrUpdate.TabInfo.cshtml deleted file mode 100644 index e601269fa7..0000000000 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/CreateOrUpdate.TabInfo.cshtml +++ /dev/null @@ -1,152 +0,0 @@ -@model MerchandiseReturnModel -@inject AdminAreaSettings adminAreaSettings -@inject OrderSettings orderSettings - -
- -
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- - -
-
-
- -
- - -
-
-
- -
- - -
-
- @if (orderSettings.MerchandiseReturns_AllowToSpecifyPickupDate) - { -
- -
- -
-
- } -
- -
- -
-
-
- -
- - -
-
-
-
-
-
- -
- - - \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/CreateOrUpdate.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/CreateOrUpdate.cshtml deleted file mode 100644 index 0efc911eaf..0000000000 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/CreateOrUpdate.cshtml +++ /dev/null @@ -1,59 +0,0 @@ -@using Grand.Business.Core.Interfaces.Common.Security -@using Grand.Domain.Permissions -@model MerchandiseReturnModel -@inject OrderSettings orderSettings -@inject IPermissionService permissionService -@{ - //has "Manage Documents" permission? - var canManageDocuments = await permissionService.Authorize(StandardPermission.ManageDocuments); -} -
- - - - - -
- -
-
-
- @if (orderSettings.MerchandiseReturns_AllowToSpecifyPickupAddress) - { - - -
- -
-
-
- } - @if (canManageDocuments) - { - - -
- -
-
-
- } - - -
- -
-
-
- - -
-
- -
-
-
-
- -
-
\ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/MerchandiseReturnNotes.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/MerchandiseReturnNotes.cshtml deleted file mode 100644 index 62aeced99b..0000000000 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/MerchandiseReturnNotes.cshtml +++ /dev/null @@ -1,206 +0,0 @@ -@using Grand.Domain.Media -@model MerchandiseReturnModel -@{ - ViewData["DownloadType"] = DownloadType.MerchandiseReturn; - ViewData["ReferenceId"] = Model.Id; -} - -
- -
-
-
- -
- -

- - @Loc["Admin.Orders.MerchandiseReturns.MerchandiseReturnNotes.AddTitle"] - -

- - -
-
-
- -
- - -
-
-
- -
- -
- - -
-
-
-
- -
- - -
-
-
-
- -
-
-
-
- - \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/WidgetZone.DetailsButtons.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/WidgetZone.DetailsButtons.cshtml new file mode 100644 index 0000000000..33a50896ea --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/WidgetZone.DetailsButtons.cshtml @@ -0,0 +1,5 @@ +@* vc:store-widget, not vc:admin-widget as the pre-consolidation Store views used — that tag helper + is not registered in the Store app (no ProjectReference to Grand.Web.Admin); the original markup + was inert and never rendered anything. Fixed during ARCH-001 Phase 8 consolidation. *@ +@model MerchandiseReturnModel + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/WidgetZone.DetailsTabs.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/WidgetZone.DetailsTabs.cshtml new file mode 100644 index 0000000000..6f16abf384 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/WidgetZone.DetailsTabs.cshtml @@ -0,0 +1,5 @@ +@* vc:store-widget, not vc:admin-widget as the pre-consolidation Store views used — that tag helper + is not registered in the Store app (no ProjectReference to Grand.Web.Admin); the original markup + was inert and never rendered anything. Fixed during ARCH-001 Phase 8 consolidation. *@ +@model MerchandiseReturnModel + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/WidgetZone.Documents.Bottom.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/WidgetZone.Documents.Bottom.cshtml new file mode 100644 index 0000000000..6f553a566d --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/WidgetZone.Documents.Bottom.cshtml @@ -0,0 +1,5 @@ +@* vc:store-widget, not vc:admin-widget as the pre-consolidation Store views used — that tag helper + is not registered in the Store app (no ProjectReference to Grand.Web.Admin); the original markup + was inert and never rendered anything. Fixed during ARCH-001 Phase 8 consolidation. *@ +@model MerchandiseReturnModel + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/WidgetZone.Documents.Top.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/WidgetZone.Documents.Top.cshtml new file mode 100644 index 0000000000..d9bbeb3469 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/WidgetZone.Documents.Top.cshtml @@ -0,0 +1,5 @@ +@* vc:store-widget, not vc:admin-widget as the pre-consolidation Store views used — that tag helper + is not registered in the Store app (no ProjectReference to Grand.Web.Admin); the original markup + was inert and never rendered anything. Fixed during ARCH-001 Phase 8 consolidation. *@ +@model MerchandiseReturnModel + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/WidgetZone.ListButtons.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/WidgetZone.ListButtons.cshtml new file mode 100644 index 0000000000..073bac7b95 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/WidgetZone.ListButtons.cshtml @@ -0,0 +1,5 @@ +@* vc:store-widget, not vc:admin-widget as the pre-consolidation Store views used — that tag helper + is not registered in the Store app (no ProjectReference to Grand.Web.Admin); the original markup + was inert and never rendered anything. Fixed during ARCH-001 Phase 8 consolidation. *@ +@model MerchandiseReturnListModel + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/WidgetZone.Notes.Bottom.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/WidgetZone.Notes.Bottom.cshtml new file mode 100644 index 0000000000..869019d5ee --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/WidgetZone.Notes.Bottom.cshtml @@ -0,0 +1,5 @@ +@* vc:store-widget, not vc:admin-widget as the pre-consolidation Store views used — that tag helper + is not registered in the Store app (no ProjectReference to Grand.Web.Admin); the original markup + was inert and never rendered anything. Fixed during ARCH-001 Phase 8 consolidation. *@ +@model MerchandiseReturnModel + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/WidgetZone.Notes.Top.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/WidgetZone.Notes.Top.cshtml new file mode 100644 index 0000000000..5c31025268 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/WidgetZone.Notes.Top.cshtml @@ -0,0 +1,5 @@ +@* vc:store-widget, not vc:admin-widget as the pre-consolidation Store views used — that tag helper + is not registered in the Store app (no ProjectReference to Grand.Web.Admin); the original markup + was inert and never rendered anything. Fixed during ARCH-001 Phase 8 consolidation. *@ +@model MerchandiseReturnModel + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/WidgetZone.TabAddress.Bottom.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/WidgetZone.TabAddress.Bottom.cshtml new file mode 100644 index 0000000000..a6ac70feac --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/WidgetZone.TabAddress.Bottom.cshtml @@ -0,0 +1,5 @@ +@* vc:store-widget, not vc:admin-widget as the pre-consolidation Store views used — that tag helper + is not registered in the Store app (no ProjectReference to Grand.Web.Admin); the original markup + was inert and never rendered anything. Fixed during ARCH-001 Phase 8 consolidation. *@ +@model MerchandiseReturnModel + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/WidgetZone.TabAddress.Top.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/WidgetZone.TabAddress.Top.cshtml new file mode 100644 index 0000000000..2c402ba181 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/WidgetZone.TabAddress.Top.cshtml @@ -0,0 +1,5 @@ +@* vc:store-widget, not vc:admin-widget as the pre-consolidation Store views used — that tag helper + is not registered in the Store app (no ProjectReference to Grand.Web.Admin); the original markup + was inert and never rendered anything. Fixed during ARCH-001 Phase 8 consolidation. *@ +@model MerchandiseReturnModel + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/WidgetZone.TabInfo.Bottom.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/WidgetZone.TabInfo.Bottom.cshtml new file mode 100644 index 0000000000..42e52a889e --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/WidgetZone.TabInfo.Bottom.cshtml @@ -0,0 +1,5 @@ +@* vc:store-widget, not vc:admin-widget as the pre-consolidation Store views used — that tag helper + is not registered in the Store app (no ProjectReference to Grand.Web.Admin); the original markup + was inert and never rendered anything. Fixed during ARCH-001 Phase 8 consolidation. *@ +@model MerchandiseReturnModel + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/WidgetZone.TabInfo.Top.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/WidgetZone.TabInfo.Top.cshtml new file mode 100644 index 0000000000..18cb6c6434 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/MerchandiseReturn/Partials/WidgetZone.TabInfo.Top.cshtml @@ -0,0 +1,5 @@ +@* vc:store-widget, not vc:admin-widget as the pre-consolidation Store views used — that tag helper + is not registered in the Store app (no ProjectReference to Grand.Web.Admin); the original markup + was inert and never rendered anything. Fixed during ARCH-001 Phase 8 consolidation. *@ +@model MerchandiseReturnModel + diff --git a/src/Web/Grand.Web.Store/Controllers/MerchandiseReturnController.cs b/src/Web/Grand.Web.Store/Controllers/MerchandiseReturnController.cs index c2bfcab1e0..df460cebf8 100644 --- a/src/Web/Grand.Web.Store/Controllers/MerchandiseReturnController.cs +++ b/src/Web/Grand.Web.Store/Controllers/MerchandiseReturnController.cs @@ -1,257 +1,35 @@ -using Grand.Business.Core.Interfaces.Checkout.Orders; -using Grand.Business.Core.Interfaces.Common.Addresses; +using Grand.Business.Core.Interfaces.Checkout.Orders; using Grand.Business.Core.Interfaces.Common.Localization; -using Grand.Domain.Common; using Grand.Domain.Orders; -using Grand.Domain.Permissions; -using Grand.Infrastructure; -using Grand.Web.AdminShared.Extensions; +using Grand.Web.AdminShared.Controllers; using Grand.Web.AdminShared.Interfaces; -using Grand.Web.AdminShared.Models.Orders; -using Grand.Web.Common.DataSource; using Grand.Web.Common.Filters; -using Grand.Web.Common.Security.Authorization; +using Grand.Web.Store.Extensions; using Microsoft.AspNetCore.Mvc; namespace Grand.Web.Store.Controllers; -[PermissionAuthorize(PermissionSystemName.MerchandiseReturns)] -public class MerchandiseReturnController : BaseStoreController +// Concrete host subclass of BaseMerchandiseReturnController (ARCH-001 MerchandiseReturn +// consolidation). This class supplies Store's DI wiring plus the attributes that used to arrive +// transitively via BaseStoreController - BaseMerchandiseReturnController can't inherit any single +// host's base controller (it's shared across Admin/Store/Vendor, each with a different +// [Area]/[Authorize*] pair), so each subclass restates its own host's attribute set explicitly, same +// pattern as OrderController. +[AutoValidateAntiforgeryToken] +[Area(Constants.AreaStore)] +[AuthorizeStore] +[AuthorizeMenu] +public class MerchandiseReturnController( + IMerchandiseReturnViewModelService merchandiseReturnViewModelService, + ITranslationService translationService, + IMerchandiseReturnService merchandiseReturnService, + IOrderService orderService, + IAdminDataScope scope) + : BaseMerchandiseReturnController(merchandiseReturnViewModelService, translationService, + merchandiseReturnService, orderService, scope) { - #region Constructors - - public MerchandiseReturnController( - IMerchandiseReturnViewModelService merchandiseReturnViewModelService, - ITranslationService translationService, - IMerchandiseReturnService merchandiseReturnService, - IOrderService orderService, - IContextAccessor contextAccessor) - { - _merchandiseReturnViewModelService = merchandiseReturnViewModelService; - _translationService = translationService; - _merchandiseReturnService = merchandiseReturnService; - _orderService = orderService; - _contextAccessor = contextAccessor; - } - - #endregion - - #region Fields - - private readonly IMerchandiseReturnViewModelService _merchandiseReturnViewModelService; - private readonly ITranslationService _translationService; - private readonly IMerchandiseReturnService _merchandiseReturnService; - private readonly IOrderService _orderService; - private readonly IContextAccessor _contextAccessor; - - #endregion Fields - - #region Methods - - //list - public IActionResult Index() - { - return RedirectToAction("List"); - } - - public IActionResult List() - { - var model = _merchandiseReturnViewModelService.PrepareReturnReqestListModel(); - return View(model); - } - - [PermissionAuthorizeAction(PermissionActionName.List)] - [HttpPost] - public async Task List(DataSourceRequest command, MerchandiseReturnListModel model) - { - model.StoreId = _contextAccessor.WorkContext.CurrentCustomer.StaffStoreId; - - var merchandiseReturnModels = await _merchandiseReturnViewModelService.PrepareMerchandiseReturnModel(model, command.Page, command.PageSize); - - var gridModel = new DataSourceResult { - Data = merchandiseReturnModels.merchandiseReturnModels, - Total = merchandiseReturnModels.totalCount - }; - - return Json(gridModel); - } - - [PermissionAuthorizeAction(PermissionActionName.Preview)] - [HttpPost] - public async Task GoToId(MerchandiseReturnListModel model) - { - if (model.GoDirectlyToId == null) - return RedirectToAction("List", "MerchandiseReturn"); - - int.TryParse(model.GoDirectlyToId, out var id); - - //try to load a product entity - var merchandiseReturn = await _merchandiseReturnService.GetMerchandiseReturnById(id); - if (merchandiseReturn == null) - //not found - return RedirectToAction("List", "MerchandiseReturn"); - - if (merchandiseReturn.StoreId != _contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) - return RedirectToAction("List", "MerchandiseReturn"); - - return RedirectToAction("Edit", "MerchandiseReturn", new { id = merchandiseReturn.Id }); - } - - [PermissionAuthorizeAction(PermissionActionName.Preview)] - [HttpPost] - public async Task ProductsForMerchandiseReturn(string merchandiseReturnId, DataSourceRequest command) - { - var merchandiseReturn = await _merchandiseReturnService.GetMerchandiseReturnById(merchandiseReturnId); - if (merchandiseReturn == null) - return ErrorForKendoGridJson("Merchandise return not found"); - - if (merchandiseReturn.StoreId != _contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) - return ErrorForKendoGridJson("Merchandise return is not your"); - - var items = await _merchandiseReturnViewModelService.PrepareMerchandiseReturnItemModel(merchandiseReturnId); - var gridModel = new DataSourceResult { - Data = items, - Total = items.Count - }; - - return Json(gridModel); - } - - //edit - [PermissionAuthorizeAction(PermissionActionName.Preview)] - public async Task Edit(string id) - { - var merchandiseReturn = await _merchandiseReturnService.GetMerchandiseReturnById(id); - if (merchandiseReturn == null) - //No merchandise return found with the specified id - return RedirectToAction("List"); - - if (merchandiseReturn.StoreId != _contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) - return RedirectToAction("List", "MerchandiseReturn"); - - var model = new MerchandiseReturnModel(); - await _merchandiseReturnViewModelService.PrepareMerchandiseReturnModel(model, merchandiseReturn, false); - return View(model); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - [HttpPost] - [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")] - public async Task Edit(MerchandiseReturnModel model, bool continueEditing, - [FromServices] IAddressAttributeService addressAttributeService, - [FromServices] IAddressAttributeParser addressAttributeParser, - [FromServices] OrderSettings orderSettings - ) - { - var merchandiseReturn = await _merchandiseReturnService.GetMerchandiseReturnById(model.Id); - if (merchandiseReturn == null) - //No merchandise return found with the specified id - return RedirectToAction("List"); - - if (merchandiseReturn.StoreId != _contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) - return RedirectToAction("List", "MerchandiseReturn"); - - if (ModelState.IsValid) - { - var customAddressAttributes = new List(); - if (orderSettings.MerchandiseReturns_AllowToSpecifyPickupAddress) - customAddressAttributes = await model.PickupAddress.ParseCustomAddressAttributes(addressAttributeParser, addressAttributeService); - - merchandiseReturn = await _merchandiseReturnViewModelService.UpdateMerchandiseReturnModel(merchandiseReturn, model, customAddressAttributes); - - Success(_translationService.GetResource("Admin.Orders.MerchandiseReturns.Updated")); - return continueEditing - ? RedirectToAction("Edit", new { id = merchandiseReturn.Id }) - : RedirectToAction("List"); - } - - //If we got this far, something failed, redisplay form - await _merchandiseReturnViewModelService.PrepareMerchandiseReturnModel(model, merchandiseReturn, false); - return View(model); - } - - //delete - [PermissionAuthorizeAction(PermissionActionName.Delete)] - [HttpPost] - public async Task Delete(string id) - { - var merchandiseReturn = await _merchandiseReturnService.GetMerchandiseReturnById(id); - if (merchandiseReturn == null) - //No merchandise return found with the specified id - return RedirectToAction("List"); - - if (merchandiseReturn.StoreId != _contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) - return RedirectToAction("List", "MerchandiseReturn"); - - if (ModelState.IsValid) - { - await _merchandiseReturnViewModelService.DeleteMerchandiseReturn(merchandiseReturn); - Success(_translationService.GetResource("Admin.Orders.MerchandiseReturns.Deleted")); - return RedirectToAction("List"); - } - - Error(ModelState); - return RedirectToAction("Edit", new { id = merchandiseReturn.Id }); - } - - #endregion - - #region Merchandise return notes - - [PermissionAuthorizeAction(PermissionActionName.Preview)] - [HttpPost] - public async Task MerchandiseReturnNotesSelect(string merchandiseReturnId, DataSourceRequest command) - { - var merchandiseReturn = await _merchandiseReturnService.GetMerchandiseReturnById(merchandiseReturnId); - if (merchandiseReturn == null) - throw new ArgumentException("No merchandise return found with the specified id"); - - if (merchandiseReturn.StoreId != _contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) return Content(""); - - //merchandise return notes - var merchandiseReturnNoteModels = await _merchandiseReturnViewModelService.PrepareMerchandiseReturnNotes(merchandiseReturn); - var gridModel = new DataSourceResult { - Data = merchandiseReturnNoteModels, - Total = merchandiseReturnNoteModels.Count - }; - return Json(gridModel); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - public async Task MerchandiseReturnNoteAdd(string merchandiseReturnId, string orderId, - string downloadId, bool displayToCustomer, string message) - { - var merchandiseReturn = await _merchandiseReturnService.GetMerchandiseReturnById(merchandiseReturnId); - if (merchandiseReturn == null) - return Json(new { Result = false }); - - var order = await _orderService.GetOrderById(orderId); - if (order == null) - return Json(new { Result = false }); - - if (merchandiseReturn.StoreId != _contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) return Json(new { Result = false }); - if (order.Id != merchandiseReturn.OrderId) return Json(new { Result = false }); - - await _merchandiseReturnViewModelService.InsertMerchandiseReturnNote(merchandiseReturn, order, downloadId, - displayToCustomer, message); - - return Json(new { Result = true }); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - [HttpPost] - public async Task MerchandiseReturnNoteDelete(string id, string merchandiseReturnId) - { - var merchandiseReturn = await _merchandiseReturnService.GetMerchandiseReturnById(merchandiseReturnId); - if (merchandiseReturn == null) - throw new ArgumentException("No merchandise return found with the specified id"); - - if (merchandiseReturn.StoreId != _contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) return Json(new { Result = false }); - - await _merchandiseReturnViewModelService.DeleteMerchandiseReturnNote(merchandiseReturn, id); - - return new JsonResult(""); - } - - #endregion -} \ No newline at end of file + // Preserved host divergence (spec §5/§11, DECIDED): Store's original soft-denies with an empty + // body instead of throwing - see BaseMerchandiseReturnController.NotFoundOrDeniedForNotesSelect's + // doc comment. + protected override IActionResult NotFoundOrDeniedForNotesSelect() => Content(""); +} 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 65a6de0cc0..8210e1d570 100644 --- a/src/Web/Grand.Web.Vendor/Areas/Vendor/Views/_ViewImports.cshtml +++ b/src/Web/Grand.Web.Vendor/Areas/Vendor/Views/_ViewImports.cshtml @@ -30,14 +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 and Shipment views bind to Grand.Web.AdminShared's Order/Shipment model families - (ARCH-001 Phase 5 Order, Phase 6 Shipment consolidation) - Grand.Web.Vendor.Models.Orders's own - OrderModel/OrderListModel and Grand.Web.Vendor.Models.Shipment (the whole namespace) are no - longer used by any controller/view. This can't be a blanket - "@using Grand.Web.AdminShared.Models.Orders" - that namespace also holds MerchandiseReturnModel/ - *ReportModel etc., which Vendor's MerchandiseReturn/Reports views still bind to their own - Grand.Web.Vendor.Models.* equivalents (not yet consolidated), so a wildcard import here would - make those bare names ambiguous instead. Alias just the types that are actually consolidated. *@ +@* 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., + which Vendor's Reports views still bind to their own Grand.Web.Vendor.Models.* equivalents (not yet + consolidated), so a wildcard import here would make those bare names ambiguous instead. Alias just + the types that are actually consolidated. *@ @using OrderModel = Grand.Web.AdminShared.Models.Orders.OrderModel; @using OrderListModel = Grand.Web.AdminShared.Models.Orders.OrderListModel; @using ShipmentModel = Grand.Web.AdminShared.Models.Orders.ShipmentModel; @@ -47,7 +46,8 @@ @using ShipmentAdminCommentModel = Grand.Web.AdminShared.Models.Orders.ShipmentAdminCommentModel; @using ShipmentDeliveryDateModel = Grand.Web.AdminShared.Models.Orders.ShipmentDeliveryDateModel; @using ShipmentShippedDateModel = Grand.Web.AdminShared.Models.Orders.ShipmentShippedDateModel; -@using Grand.Web.Vendor.Models.MerchandiseReturn; +@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 Grand.Web.Vendor.Models.Report; diff --git a/src/Web/Grand.Web.Vendor/Controllers/MerchandiseReturnController.cs b/src/Web/Grand.Web.Vendor/Controllers/MerchandiseReturnController.cs index 746210c81b..843b40005c 100644 --- a/src/Web/Grand.Web.Vendor/Controllers/MerchandiseReturnController.cs +++ b/src/Web/Grand.Web.Vendor/Controllers/MerchandiseReturnController.cs @@ -1,225 +1,29 @@ -using Grand.Business.Core.Interfaces.Checkout.Orders; -using Grand.Business.Core.Interfaces.Common.Addresses; +using Grand.Business.Core.Interfaces.Checkout.Orders; using Grand.Business.Core.Interfaces.Common.Localization; -using Grand.Domain.Permissions; -using Grand.Domain.Common; using Grand.Domain.Orders; -using Grand.Infrastructure; -using Grand.Web.Common.DataSource; +using Grand.Web.AdminShared.Controllers; +using Grand.Web.AdminShared.Interfaces; using Grand.Web.Common.Filters; -using Grand.Web.Common.Security.Authorization; using Grand.Web.Vendor.Extensions; -using Grand.Web.Vendor.Interfaces; -using Grand.Web.Vendor.Models.MerchandiseReturn; using Microsoft.AspNetCore.Mvc; namespace Grand.Web.Vendor.Controllers; -[PermissionAuthorize(PermissionSystemName.MerchandiseReturns)] -public class MerchandiseReturnController : BaseVendorController -{ - #region Constructors - - public MerchandiseReturnController( - IMerchandiseReturnViewModelService merchandiseReturnViewModelService, - ITranslationService translationService, - IMerchandiseReturnService merchandiseReturnService, - IContextAccessor contextAccessor) - { - _merchandiseReturnViewModelService = merchandiseReturnViewModelService; - _translationService = translationService; - _merchandiseReturnService = merchandiseReturnService; - _contextAccessor = contextAccessor; - } - - #endregion - - #region Fields - - private readonly IMerchandiseReturnViewModelService _merchandiseReturnViewModelService; - private readonly ITranslationService _translationService; - private readonly IMerchandiseReturnService _merchandiseReturnService; - private readonly IContextAccessor _contextAccessor; - - #endregion Fields - - #region Methods - - //list - public IActionResult Index() - { - return RedirectToAction("List"); - } - - public IActionResult List() - { - var model = _merchandiseReturnViewModelService.PrepareReturnRequestListModel(); - return View(model); - } - - [PermissionAuthorizeAction(PermissionActionName.List)] - [HttpPost] - public async Task List(DataSourceRequest command, MerchandiseReturnListModel model) - { - var merchandiseReturnModels = - await _merchandiseReturnViewModelService.PrepareMerchandiseReturnModel(model, command.Page, - command.PageSize); - var gridModel = new DataSourceResult { - Data = merchandiseReturnModels.merchandiseReturnModels, - Total = merchandiseReturnModels.totalCount - }; - - return Json(gridModel); - } - - [PermissionAuthorizeAction(PermissionActionName.Preview)] - [HttpPost] - public async Task GoToId(MerchandiseReturnListModel model) - { - var merchandiseReturn = await _merchandiseReturnService.GetMerchandiseReturnById(model.GoDirectlyToId); - if (merchandiseReturn == null || !_contextAccessor.WorkContext.HasAccessToMerchandiseReturn(merchandiseReturn)) - //not found - return RedirectToAction("List", "MerchandiseReturn"); - - return RedirectToAction("Edit", "MerchandiseReturn", new { id = merchandiseReturn.Id }); - } - - [PermissionAuthorizeAction(PermissionActionName.Preview)] - [HttpPost] - public async Task ProductsForMerchandiseReturn(string merchandiseReturnId) - { - var merchandiseReturn = await _merchandiseReturnService.GetMerchandiseReturnById(merchandiseReturnId); - if (merchandiseReturn == null || !_contextAccessor.WorkContext.HasAccessToMerchandiseReturn(merchandiseReturn)) - return ErrorForKendoGridJson("Merchandise return not found"); - - var items = await _merchandiseReturnViewModelService.PrepareMerchandiseReturnItemModel(merchandiseReturnId); - var gridModel = new DataSourceResult { - Data = items, - Total = items.Count - }; - - return Json(gridModel); - } - - //edit - [PermissionAuthorizeAction(PermissionActionName.Preview)] - public async Task Edit(string id) - { - var merchandiseReturn = await _merchandiseReturnService.GetMerchandiseReturnById(id); - if (merchandiseReturn == null || !_contextAccessor.WorkContext.HasAccessToMerchandiseReturn(merchandiseReturn)) - //No merchandise return found with the specified id - return RedirectToAction("List"); - - var model = new MerchandiseReturnModel(); - await _merchandiseReturnViewModelService.PrepareMerchandiseReturnModel(model, merchandiseReturn, false); - return View(model); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - [HttpPost] - [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")] - public async Task Edit(MerchandiseReturnModel model, bool continueEditing, - [FromServices] IAddressAttributeService addressAttributeService, - [FromServices] IAddressAttributeParser addressAttributeParser, - [FromServices] OrderSettings orderSettings - ) - { - var merchandiseReturn = await _merchandiseReturnService.GetMerchandiseReturnById(model.Id); - if (merchandiseReturn == null || !_contextAccessor.WorkContext.HasAccessToMerchandiseReturn(merchandiseReturn)) - //No merchandise return found with the specified id - return RedirectToAction("List"); - - if (ModelState.IsValid) - { - var customAddressAttributes = new List(); - if (orderSettings.MerchandiseReturns_AllowToSpecifyPickupAddress) - customAddressAttributes = - await model.PickupAddress.ParseCustomAddressAttributes(addressAttributeParser, - addressAttributeService); - merchandiseReturn = - await _merchandiseReturnViewModelService.UpdateMerchandiseReturnModel(merchandiseReturn, model, - customAddressAttributes); - - Success(_translationService.GetResource("Vendor.Orders.MerchandiseReturns.Updated")); - return continueEditing - ? RedirectToAction("Edit", new { id = merchandiseReturn.Id }) - : RedirectToAction("List"); - } - - //If we got this far, something failed, redisplay form - await _merchandiseReturnViewModelService.PrepareMerchandiseReturnModel(model, merchandiseReturn, false); - return View(model); - } - - //delete - [PermissionAuthorizeAction(PermissionActionName.Delete)] - [HttpPost] - public async Task Delete(string id) - { - var merchandiseReturn = await _merchandiseReturnService.GetMerchandiseReturnById(id); - if (merchandiseReturn == null || !_contextAccessor.WorkContext.HasAccessToMerchandiseReturn(merchandiseReturn)) - //No merchandise return found with the specified id - return RedirectToAction("List"); - - if (ModelState.IsValid) - { - await _merchandiseReturnViewModelService.DeleteMerchandiseReturn(merchandiseReturn); - Success(_translationService.GetResource("Vendor.Orders.MerchandiseReturns.Deleted")); - return RedirectToAction("List"); - } - - Error(ModelState); - return RedirectToAction("Edit", new { id = merchandiseReturn.Id }); - } - - #endregion - - #region Merchandise return notes - - [PermissionAuthorizeAction(PermissionActionName.Preview)] - [HttpPost] - public async Task MerchandiseReturnNotesSelect(string merchandiseReturnId) - { - var merchandiseReturn = await _merchandiseReturnService.GetMerchandiseReturnById(merchandiseReturnId); - if (merchandiseReturn == null || !_contextAccessor.WorkContext.HasAccessToMerchandiseReturn(merchandiseReturn)) - throw new ArgumentException("No merchandise return found with the specified id"); - - //merchandise return notes - var merchandiseReturnNoteModels = - await _merchandiseReturnViewModelService.PrepareMerchandiseReturnNotes(merchandiseReturn); - var gridModel = new DataSourceResult { - Data = merchandiseReturnNoteModels, - Total = merchandiseReturnNoteModels.Count - }; - return Json(gridModel); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - public async Task MerchandiseReturnNoteAdd(string merchandiseReturnId, bool displayToCustomer, - string message) - { - var merchandiseReturn = await _merchandiseReturnService.GetMerchandiseReturnById(merchandiseReturnId); - if (merchandiseReturn == null || !_contextAccessor.WorkContext.HasAccessToMerchandiseReturn(merchandiseReturn)) - return Json(new { Result = false }); - - await _merchandiseReturnViewModelService.InsertMerchandiseReturnNote(merchandiseReturn, displayToCustomer, - message); - - return Json(new { Result = true }); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - [HttpPost] - public async Task MerchandiseReturnNoteDelete(string id, string merchandiseReturnId) - { - var merchandiseReturn = await _merchandiseReturnService.GetMerchandiseReturnById(merchandiseReturnId); - if (merchandiseReturn == null || !_contextAccessor.WorkContext.HasAccessToMerchandiseReturn(merchandiseReturn)) - throw new ArgumentException("No merchandise return found with the specified id"); - - await _merchandiseReturnViewModelService.DeleteMerchandiseReturnNote(merchandiseReturn, id); - - return new JsonResult(""); - } - - #endregion -} \ No newline at end of file +// Concrete host subclass of BaseMerchandiseReturnController (ARCH-001 MerchandiseReturn +// consolidation). This class supplies Vendor's DI wiring plus the attributes that used to arrive +// transitively via BaseVendorController - BaseMerchandiseReturnController can't inherit any single +// host's base controller (it's shared across Admin/Store/Vendor, each with a different +// [Area]/[Authorize*] pair), so each subclass restates its own host's attribute set explicitly, same +// pattern as OrderController. +[AutoValidateAntiforgeryToken] +[Area(Constants.AreaVendor)] +[AuthorizeVendor] +[AuthorizeMenu] +public class MerchandiseReturnController( + IMerchandiseReturnViewModelService merchandiseReturnViewModelService, + ITranslationService translationService, + IMerchandiseReturnService merchandiseReturnService, + IOrderService orderService, + IAdminDataScope scope) + : BaseMerchandiseReturnController(merchandiseReturnViewModelService, translationService, + merchandiseReturnService, orderService, scope); diff --git a/src/Web/Grand.Web.Vendor/Extensions/HasAccess.cs b/src/Web/Grand.Web.Vendor/Extensions/HasAccess.cs index e149d85be5..d62595943f 100644 --- a/src/Web/Grand.Web.Vendor/Extensions/HasAccess.cs +++ b/src/Web/Grand.Web.Vendor/Extensions/HasAccess.cs @@ -51,11 +51,4 @@ public static bool HasAccessToVendorReview(this IWorkContext workContext, Vendor return vendorReview.VendorId == workContext.CurrentVendor.Id; } - - public static bool HasAccessToMerchandiseReturn(this IWorkContext workContext, MerchandiseReturn merchandiseReturn) - { - ArgumentNullException.ThrowIfNull(merchandiseReturn); - - return merchandiseReturn.VendorId == workContext.CurrentVendor.Id; - } } \ No newline at end of file diff --git a/src/Web/Grand.Web.Vendor/Interfaces/IMerchandiseReturnViewModelService.cs b/src/Web/Grand.Web.Vendor/Interfaces/IMerchandiseReturnViewModelService.cs deleted file mode 100644 index 40d38551a8..0000000000 --- a/src/Web/Grand.Web.Vendor/Interfaces/IMerchandiseReturnViewModelService.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Grand.Domain.Common; -using Grand.Domain.Orders; -using Grand.Web.Vendor.Models.MerchandiseReturn; - -namespace Grand.Web.Vendor.Interfaces; - -public interface IMerchandiseReturnViewModelService -{ - Task PrepareMerchandiseReturnModel(MerchandiseReturnModel model, - MerchandiseReturn merchandiseReturn, bool excludeProperties); - - Task<(IList merchandiseReturnModels, int totalCount)> PrepareMerchandiseReturnModel( - MerchandiseReturnListModel model, int pageIndex, int pageSize); - - MerchandiseReturnListModel PrepareReturnRequestListModel(); - - Task> PrepareMerchandiseReturnItemModel( - string merchandiseReturnId); - - Task UpdateMerchandiseReturnModel(MerchandiseReturn merchandiseReturn, - MerchandiseReturnModel model, List customAddressAttributes); - - Task DeleteMerchandiseReturn(MerchandiseReturn merchandiseReturn); - - Task> PrepareMerchandiseReturnNotes( - MerchandiseReturn merchandiseReturn); - - Task InsertMerchandiseReturnNote(MerchandiseReturn merchandiseReturn, bool displayToCustomer, string message); - Task DeleteMerchandiseReturnNote(MerchandiseReturn merchandiseReturn, string id); -} \ No newline at end of file diff --git a/src/Web/Grand.Web.Vendor/Models/MerchandiseReturn/MerchandiseReturnListModel.cs b/src/Web/Grand.Web.Vendor/Models/MerchandiseReturn/MerchandiseReturnListModel.cs deleted file mode 100644 index ad2e54eb6c..0000000000 --- a/src/Web/Grand.Web.Vendor/Models/MerchandiseReturn/MerchandiseReturnListModel.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Grand.Infrastructure.ModelBinding; -using Grand.Infrastructure.Models; -using Microsoft.AspNetCore.Mvc.Rendering; -using System.ComponentModel.DataAnnotations; - -namespace Grand.Web.Vendor.Models.MerchandiseReturn; - -public class MerchandiseReturnListModel : BaseModel -{ - [GrandResourceDisplayName("Admin.Orders.MerchandiseReturns.List.SearchCustomerEmail")] - public string SearchCustomerEmail { get; set; } - - [GrandResourceDisplayName("Admin.Orders.MerchandiseReturns.List.SearchMerchandiseReturnStatus")] - public int SearchMerchandiseReturnStatusId { get; set; } - - [GrandResourceDisplayName("Admin.Orders.MerchandiseReturns.List.GoDirectlyToId")] - public int GoDirectlyToId { get; set; } - - [GrandResourceDisplayName("Admin.Orders.MerchandiseReturns.List.StartDate")] - [UIHint("DateNullable")] - public DateTime? StartDate { get; set; } - - [GrandResourceDisplayName("Admin.Orders.MerchandiseReturns.List.EndDate")] - [UIHint("DateNullable")] - public DateTime? EndDate { get; set; } - - public IList MerchandiseReturnStatus { get; set; } = new List(); -} \ No newline at end of file diff --git a/src/Web/Grand.Web.Vendor/Models/MerchandiseReturn/MerchandiseReturnModel.cs b/src/Web/Grand.Web.Vendor/Models/MerchandiseReturn/MerchandiseReturnModel.cs deleted file mode 100644 index 74e93d358f..0000000000 --- a/src/Web/Grand.Web.Vendor/Models/MerchandiseReturn/MerchandiseReturnModel.cs +++ /dev/null @@ -1,102 +0,0 @@ -using Grand.Infrastructure.ModelBinding; -using Grand.Infrastructure.Models; -using Grand.Web.Vendor.Models.Common; - -namespace Grand.Web.Vendor.Models.MerchandiseReturn; - -public class MerchandiseReturnModel : BaseEntityModel -{ - [GrandResourceDisplayName("Vendor.Orders.MerchandiseReturns.Fields.ID")] - public override string Id { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.MerchandiseReturns.Fields.ID")] - public int ReturnNumber { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.MerchandiseReturns.Fields.Order")] - public string OrderId { get; set; } - - public int OrderNumber { get; set; } - public string OrderCode { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.MerchandiseReturns.Fields.ExternalId")] - public string ExternalId { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.MerchandiseReturns.Fields.Customer")] - public string CustomerId { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.MerchandiseReturns.Fields.Customer")] - public string CustomerInfo { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.MerchandiseReturns.Fields.Total")] - public string Total { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.MerchandiseReturns.Fields.CustomerComments")] - public string CustomerComments { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.MerchandiseReturns.Fields.StaffNotes")] - public string StaffNotes { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.MerchandiseReturns.Fields.Status")] - public int MerchandiseReturnStatusId { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.MerchandiseReturns.Fields.Status")] - public string MerchandiseReturnStatusStr { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.MerchandiseReturns.Fields.CreatedOn")] - public DateTime CreatedOn { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.MerchandiseReturns.Fields.Quantity")] - public int Quantity { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.MerchandiseReturns.Fields.PickupDate")] - public DateTime PickupDate { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.MerchandiseReturns.Fields.PickupAddress")] - public AddressModel PickupAddress { get; set; } - - public List Items { get; set; } = new(); - - [GrandResourceDisplayName("Vendor.Orders.MerchandiseReturns.NotifyCustomer")] - public bool NotifyCustomer { get; set; } - - //merchandise return notes - [GrandResourceDisplayName("Vendor.Orders.MerchandiseReturns.MerchandiseReturnNotes.Fields.DisplayToCustomer")] - public bool AddMerchandiseReturnNoteDisplayToCustomer { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.MerchandiseReturns.MerchandiseReturnNotes.Fields.Note")] - public string AddMerchandiseReturnNoteMessage { get; set; } - - public class MerchandiseReturnItemModel : BaseEntityModel - { - public string ProductId { get; set; } - - public string ProductName { get; set; } - - public string ProductSku { get; set; } - - public string UnitPrice { get; set; } - - public int Quantity { get; set; } - - public string ReasonForReturn { get; set; } - - public string RequestedAction { get; set; } - } - - public class MerchandiseReturnNote : BaseEntityModel - { - public string MerchandiseReturnId { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.MerchandiseReturns.MerchandiseReturnNotes.Fields.DisplayToCustomer")] - public bool DisplayToCustomer { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.MerchandiseReturns.MerchandiseReturnNotes.Fields.Note")] - public string Note { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.MerchandiseReturns.MerchandiseReturnNotes.Fields.CreatedOn")] - public DateTime CreatedOn { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.MerchandiseReturns.MerchandiseReturnNotes.Fields.CreatedByCustomer")] - public bool CreatedByCustomer { get; set; } - } -} \ No newline at end of file diff --git a/src/Web/Grand.Web.Vendor/Services/MerchandiseReturnViewModelService.cs b/src/Web/Grand.Web.Vendor/Services/MerchandiseReturnViewModelService.cs deleted file mode 100644 index 0c0bb2400c..0000000000 --- a/src/Web/Grand.Web.Vendor/Services/MerchandiseReturnViewModelService.cs +++ /dev/null @@ -1,361 +0,0 @@ -using Grand.Business.Core.Interfaces.Catalog.Prices; -using Grand.Business.Core.Interfaces.Catalog.Products; -using Grand.Business.Core.Interfaces.Checkout.Orders; -using Grand.Business.Core.Interfaces.Common.Addresses; -using Grand.Business.Core.Interfaces.Common.Directory; -using Grand.Business.Core.Interfaces.Common.Localization; -using Grand.Business.Core.Interfaces.Customers; -using Grand.Business.Core.Interfaces.Messages; -using Grand.Domain.Common; -using Grand.Domain.Directory; -using Grand.Domain.Localization; -using Grand.Domain.Orders; -using Grand.Infrastructure; -using Grand.Infrastructure.Configuration; -using Grand.Web.Common.Localization; -using Grand.Web.Vendor.Extensions; -using Grand.Web.Vendor.Interfaces; -using Grand.Web.Vendor.Models.Common; -using Grand.Web.Vendor.Models.MerchandiseReturn; -using Microsoft.AspNetCore.Mvc.Rendering; - -namespace Grand.Web.Vendor.Services; - -public class MerchandiseReturnViewModelService : IMerchandiseReturnViewModelService -{ - - #region Fields - - private readonly IOrderService _orderService; - private readonly IContextAccessor _contextAccessor; - private readonly IProductService _productService; - private readonly IDateTimeService _dateTimeService; - private readonly ICustomerService _customerService; - private readonly ITranslationService _translationService; - private readonly IMessageProviderService _messageProviderService; - private readonly LanguageSettings _languageSettings; - private readonly IMerchandiseReturnService _merchandiseReturnService; - private readonly IPriceFormatter _priceFormatter; - private readonly AddressSettings _addressSettings; - private readonly OrderSettings _orderSettings; - private readonly ICountryService _countryService; - private readonly IAddressAttributeService _addressAttributeService; - private readonly IAddressAttributeParser _addressAttributeParser; - private readonly IEnumTranslationService _enumTranslationService; - private readonly CustomerConfig _customerConfig; - - #endregion Fields - - #region Constructors - - public MerchandiseReturnViewModelService( - IOrderService orderService, - IContextAccessor contextAccessor, - IProductService productService, - ICustomerService customerService, - IDateTimeService dateTimeService, - ITranslationService translationService, - IMessageProviderService messageProviderService, - LanguageSettings languageSettings, - IMerchandiseReturnService merchandiseReturnService, - IPriceFormatter priceFormatter, - AddressSettings addressSettings, - ICountryService countryService, - IAddressAttributeService addressAttributeService, - IAddressAttributeParser addressAttributeParser, - OrderSettings orderSettings, - IEnumTranslationService enumTranslationService, - CustomerConfig customerConfig) - { - _orderService = orderService; - _contextAccessor = contextAccessor; - _productService = productService; - _customerService = customerService; - _dateTimeService = dateTimeService; - _translationService = translationService; - _messageProviderService = messageProviderService; - _languageSettings = languageSettings; - _merchandiseReturnService = merchandiseReturnService; - _priceFormatter = priceFormatter; - _addressSettings = addressSettings; - _countryService = countryService; - _addressAttributeService = addressAttributeService; - _addressAttributeParser = addressAttributeParser; - _orderSettings = orderSettings; - _enumTranslationService = enumTranslationService; - _customerConfig = customerConfig; - } - - #endregion - - public virtual async Task PrepareMerchandiseReturnModel(MerchandiseReturnModel model, - MerchandiseReturn merchandiseReturn, bool excludeProperties) - { - ArgumentNullException.ThrowIfNull(model); - ArgumentNullException.ThrowIfNull(merchandiseReturn); - - var order = await _orderService.GetOrderById(merchandiseReturn.OrderId); - double unitPriceInclTaxInCustomerCurrency = 0; - foreach (var item in merchandiseReturn.MerchandiseReturnItems) - { - var orderItem = order.OrderItems.First(x => x.Id == item.OrderItemId); - unitPriceInclTaxInCustomerCurrency += orderItem.UnitPriceInclTax * item.Quantity; - } - - model.Total = _priceFormatter.FormatPrice(unitPriceInclTaxInCustomerCurrency); - model.Quantity = merchandiseReturn.MerchandiseReturnItems.Sum(x => x.Quantity); - model.Id = merchandiseReturn.Id; - model.OrderId = order.Id; - model.OrderNumber = order.OrderNumber; - model.OrderCode = order.Code; - model.ReturnNumber = merchandiseReturn.ReturnNumber; - model.CustomerId = merchandiseReturn.CustomerId; - model.NotifyCustomer = merchandiseReturn.NotifyCustomer; - var customer = await _customerService.GetCustomerById(merchandiseReturn.CustomerId); - if (customer != null) - model.CustomerInfo = !string.IsNullOrEmpty(customer.Email) - ? customer.Email - : _translationService.GetResource("Admin.Customers.Guest"); - else - model.CustomerInfo = _translationService.GetResource("Admin.Customers.Guest"); - - model.MerchandiseReturnStatusStr = merchandiseReturn.MerchandiseReturnStatus.ToString(); - model.CreatedOn = _dateTimeService.ConvertToUserTime(merchandiseReturn.CreatedOnUtc, DateTimeKind.Utc); - model.PickupDate = merchandiseReturn.PickupDate; - model.UserFields = merchandiseReturn.UserFields; - - if (!excludeProperties) - { - var addr = new AddressModel(); - model.PickupAddress = await PrepareAddressModel(addr, merchandiseReturn.PickupAddress, false); - model.CustomerComments = merchandiseReturn.CustomerComments; - model.ExternalId = merchandiseReturn.ExternalId; - model.StaffNotes = merchandiseReturn.StaffNotes; - model.MerchandiseReturnStatusId = merchandiseReturn.MerchandiseReturnStatusId; - } - - return model; - } - - public virtual async Task<(IList merchandiseReturnModels, int totalCount)> - PrepareMerchandiseReturnModel(MerchandiseReturnListModel model, int pageIndex, int pageSize) - { - var customerId = string.Empty; - if (!string.IsNullOrEmpty(model.SearchCustomerEmail)) - { - //with per-store customer identity the same e-mail may exist in several stores - scope the - //lookup to the current store so the search matches this store's customer - var storeId = _customerConfig.RegisterCustomersPerStore - ? _contextAccessor.StoreContext.CurrentStore.Id - : ""; - var customer = await _customerService.GetCustomerByEmail(model.SearchCustomerEmail, storeId); - customerId = customer != null ? customer.Id : "00000000-0000-0000-0000-000000000000"; - } - - DateTime? startDateValue = model.StartDate == null - ? null - : _dateTimeService.ConvertToUtcTime(model.StartDate.Value, _dateTimeService.CurrentTimeZone); - - DateTime? endDateValue = model.EndDate == null - ? null - : _dateTimeService.ConvertToUtcTime(model.EndDate.Value, _dateTimeService.CurrentTimeZone); - - var merchandiseReturns = await _merchandiseReturnService.SearchMerchandiseReturns( - customerId: customerId, - vendorId: _contextAccessor.WorkContext.CurrentVendor.Id, - rs: model.SearchMerchandiseReturnStatusId >= 0 - ? (MerchandiseReturnStatus?)model.SearchMerchandiseReturnStatusId - : null, - pageIndex: pageIndex - 1, - pageSize: pageSize, - createdFromUtc: startDateValue, - createdToUtc: endDateValue); - var merchandiseReturnModels = new List(); - foreach (var rr in merchandiseReturns) - { - var merchandiseReturnModel = new MerchandiseReturnModel(); - merchandiseReturnModels.Add(await PrepareMerchandiseReturnModel(merchandiseReturnModel, rr, true)); - } - - return (merchandiseReturnModels, merchandiseReturns.TotalCount); - } - - public virtual MerchandiseReturnListModel PrepareReturnRequestListModel() - { - var model = new MerchandiseReturnListModel { - //Merchandise return status - MerchandiseReturnStatus = _enumTranslationService.ToSelectList(MerchandiseReturnStatus.Pending, false).ToList() - }; - model.MerchandiseReturnStatus.Insert(0, - new SelectListItem { Text = _translationService.GetResource("Admin.Common.All"), Value = "-1" }); - - return model; - } - - public virtual async Task> - PrepareMerchandiseReturnItemModel(string merchandiseReturnId) - { - var merchandiseReturn = await _merchandiseReturnService.GetMerchandiseReturnById(merchandiseReturnId); - var items = new List(); - var order = await _orderService.GetOrderById(merchandiseReturn.OrderId); - - foreach (var item in merchandiseReturn.MerchandiseReturnItems) - { - var orderItem = order.OrderItems.FirstOrDefault(x => x.Id == item.OrderItemId); - ArgumentNullException.ThrowIfNull(orderItem); - items.Add(new MerchandiseReturnModel.MerchandiseReturnItemModel { - ProductId = orderItem.ProductId, - ProductName = (await _productService.GetProductByIdIncludeArch(orderItem.ProductId)).Name, - ProductSku = orderItem.Sku, - Quantity = item.Quantity, - UnitPrice = _priceFormatter.FormatPrice(orderItem!.UnitPriceInclTax), - ReasonForReturn = item.ReasonForReturn, - RequestedAction = item.RequestedAction - }); - } - - return items; - } - - public virtual async Task UpdateMerchandiseReturnModel(MerchandiseReturn merchandiseReturn, - MerchandiseReturnModel model, List customAddressAttributes) - { - merchandiseReturn.CustomerComments = model.CustomerComments; - merchandiseReturn.StaffNotes = model.StaffNotes; - merchandiseReturn.MerchandiseReturnStatusId = model.MerchandiseReturnStatusId; - merchandiseReturn.ExternalId = model.ExternalId; - merchandiseReturn.UserFields = model.UserFields; - - if (_orderSettings.MerchandiseReturns_AllowToSpecifyPickupDate) - merchandiseReturn.PickupDate = model.PickupDate; - if (_orderSettings.MerchandiseReturns_AllowToSpecifyPickupAddress) - { - merchandiseReturn.PickupAddress = model.PickupAddress.ToEntity(); - if (merchandiseReturn.PickupAddress != null) - merchandiseReturn.PickupAddress.Attributes = customAddressAttributes; - } - - merchandiseReturn.NotifyCustomer = model.NotifyCustomer; - await _merchandiseReturnService.UpdateMerchandiseReturn(merchandiseReturn); - if (model.NotifyCustomer) - await NotifyCustomer(merchandiseReturn); - return merchandiseReturn; - } - - public virtual async Task DeleteMerchandiseReturn(MerchandiseReturn merchandiseReturn) - { - await _merchandiseReturnService.DeleteMerchandiseReturn(merchandiseReturn); - } - - public virtual async Task> PrepareMerchandiseReturnNotes( - MerchandiseReturn merchandiseReturn) - { - //merchandise return notes - var merchandiseReturnNoteModels = new List(); - foreach (var merchandiseReturnNote in (await _merchandiseReturnService.GetMerchandiseReturnNotes( - merchandiseReturn.Id)) - .OrderByDescending(on => on.CreatedOnUtc)) - merchandiseReturnNoteModels.Add(new MerchandiseReturnModel.MerchandiseReturnNote { - Id = merchandiseReturnNote.Id, - MerchandiseReturnId = merchandiseReturn.Id, - DisplayToCustomer = merchandiseReturnNote.DisplayToCustomer, - Note = merchandiseReturnNote.Note, - CreatedOn = _dateTimeService.ConvertToUserTime(merchandiseReturnNote.CreatedOnUtc, DateTimeKind.Utc), - CreatedByCustomer = merchandiseReturnNote.CreatedByCustomer - }); - return merchandiseReturnNoteModels; - } - - public virtual async Task InsertMerchandiseReturnNote(MerchandiseReturn merchandiseReturn, bool displayToCustomer, - string message) - { - var merchandiseReturnNote = new MerchandiseReturnNote { - DisplayToCustomer = displayToCustomer, - Note = message, - MerchandiseReturnId = merchandiseReturn.Id - }; - await _merchandiseReturnService.InsertMerchandiseReturnNote(merchandiseReturnNote); - - //new merchandise return notification - if (displayToCustomer) - { - //email - var order = await _orderService.GetOrderById(merchandiseReturn.OrderId); - await _messageProviderService.SendNewMerchandiseReturnNoteAddedCustomerMessage(merchandiseReturn, - merchandiseReturnNote, order); - } - } - - public virtual async Task DeleteMerchandiseReturnNote(MerchandiseReturn merchandiseReturn, string id) - { - var merchandiseReturnNote = - (await _merchandiseReturnService.GetMerchandiseReturnNotes(merchandiseReturn.Id)).FirstOrDefault(on => - on.Id == id); - if (merchandiseReturnNote == null) - throw new ArgumentException("No merchandise return note found with the specified id"); - - merchandiseReturnNote.MerchandiseReturnId = merchandiseReturn.Id; - await _merchandiseReturnService.DeleteMerchandiseReturnNote(merchandiseReturnNote); - } - - private async Task PrepareAddressModel(AddressModel model, Address address, bool excludeProperties) - { - if (address != null) - if (!excludeProperties) - model = await address.ToModel(_countryService); - - model ??= new AddressModel(); - - model.FirstNameEnabled = true; - model.FirstNameRequired = true; - model.LastNameEnabled = true; - model.LastNameRequired = true; - model.EmailEnabled = true; - model.EmailRequired = true; - model.CompanyEnabled = _addressSettings.CompanyEnabled; - model.CompanyRequired = _addressSettings.CompanyRequired; - model.VatNumberEnabled = _addressSettings.VatNumberEnabled; - model.VatNumberRequired = _addressSettings.VatNumberRequired; - model.CountryEnabled = _addressSettings.CountryEnabled; - model.StateProvinceEnabled = _addressSettings.StateProvinceEnabled; - model.CityEnabled = _addressSettings.CityEnabled; - model.CityRequired = _addressSettings.CityRequired; - model.StreetAddressEnabled = _addressSettings.StreetAddressEnabled; - model.StreetAddressRequired = _addressSettings.StreetAddressRequired; - model.StreetAddress2Enabled = _addressSettings.StreetAddress2Enabled; - model.StreetAddress2Required = _addressSettings.StreetAddress2Required; - model.ZipPostalCodeEnabled = _addressSettings.ZipPostalCodeEnabled; - model.ZipPostalCodeRequired = _addressSettings.ZipPostalCodeRequired; - model.PhoneEnabled = _addressSettings.PhoneEnabled; - model.PhoneRequired = _addressSettings.PhoneRequired; - model.FaxEnabled = _addressSettings.FaxEnabled; - model.FaxRequired = _addressSettings.FaxRequired; - model.NoteEnabled = _addressSettings.NoteEnabled; - - //countries - model.AvailableCountries.Add(new SelectListItem - { Text = _translationService.GetResource("Admin.Address.SelectCountry"), Value = "" }); - foreach (var c in await _countryService.GetAllCountries(showHidden: true)) - model.AvailableCountries.Add(new SelectListItem - { Text = c.Name, Value = c.Id, Selected = c.Id == model.CountryId }); - //states - var states = !string.IsNullOrEmpty(model.CountryId) - ? (await _countryService.GetCountryById(model.CountryId))?.StateProvinces - : new List(); - if (states is { Count: > 0 }) - foreach (var s in states) - model.AvailableStates.Add(new SelectListItem - { Text = s.Name, Value = s.Id, Selected = s.Id == model.StateProvinceId }); - //customer attribute services - await model.PrepareCustomAddressAttributes(address, _addressAttributeService, _addressAttributeParser); - - return model; - } - - private async Task NotifyCustomer(MerchandiseReturn merchandiseReturn) - { - var order = await _orderService.GetOrderById(merchandiseReturn.OrderId); - await _messageProviderService.SendMerchandiseReturnStatusChangedCustomerMessage(merchandiseReturn, order, - _languageSettings.DefaultAdminLanguageId); - } -} \ 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 08f735ef86..cc87150084 100644 --- a/src/Web/Grand.Web.Vendor/Startup/StartupApplication.cs +++ b/src/Web/Grand.Web.Vendor/Startup/StartupApplication.cs @@ -25,7 +25,10 @@ public void ConfigureServices(IServiceCollection services, IConfiguration config // 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. - services.AddScoped(); + // 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(); }