diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/AdminOrderDataScopeTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/AdminOrderDataScopeTests.cs new file mode 100644 index 000000000..86934ca11 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/AdminOrderDataScopeTests.cs @@ -0,0 +1,73 @@ +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Domain.Customers; +using Grand.Domain.Orders; +using Grand.Infrastructure; +using Grand.Web.AdminShared.Services; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Admin.Tests.Controllers; + +[TestClass] +public class AdminOrderDataScopeTests +{ + private static AdminOrderDataScope Build(bool isSalesManager, string currentCustomerSeId) + { + var customer = new Customer { SeId = currentCustomerSeId }; + var workContextMock = new Mock(); + workContextMock.Setup(w => w.CurrentCustomer).Returns(customer); + var contextAccessorMock = new Mock(); + contextAccessorMock.Setup(c => c.WorkContext).Returns(workContextMock.Object); + + var groupServiceMock = new Mock(); + groupServiceMock.Setup(g => g.IsSalesManager(customer)).ReturnsAsync(isSalesManager); + + return new AdminOrderDataScope(contextAccessorMock.Object, groupServiceMock.Object); + } + + [TestMethod] + public async Task HasAccess_NotSalesManager_TrueRegardlessOfSeId() + { + var scope = Build(isSalesManager: false, currentCustomerSeId: "se-1"); + var order = new Order { SeId = "se-2" }; + + Assert.IsTrue(await scope.HasAccess(order)); + } + + [TestMethod] + public async Task HasAccess_SalesManager_MatchingSeId_True() + { + var scope = Build(isSalesManager: true, currentCustomerSeId: "se-1"); + var order = new Order { SeId = "se-1" }; + + Assert.IsTrue(await scope.HasAccess(order)); + } + + [TestMethod] + public async Task HasAccess_SalesManager_MismatchedSeId_False() + { + var scope = Build(isSalesManager: true, currentCustomerSeId: "se-1"); + var order = new Order { SeId = "se-2" }; + + Assert.IsFalse(await scope.HasAccess(order)); + } + + [TestMethod] + public async Task HasAccess_NullEntity_False() + { + var scope = Build(isSalesManager: false, currentCustomerSeId: "se-1"); + + Assert.IsFalse(await scope.HasAccess(null)); + } + + [TestMethod] + public void ScopeDefaults_MatchGlobalAdminSemantics() + { + var scope = Build(isSalesManager: false, currentCustomerSeId: null); + + Assert.IsNull(scope.DefaultStoreId); + Assert.IsNull(scope.DefaultVendorId); + Assert.AreEqual("Admin", scope.ResourceKeyPrefix); + Assert.IsTrue(scope.ShowStoreSelector); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseOrderControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseOrderControllerTests.cs new file mode 100644 index 000000000..a84197dbb --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseOrderControllerTests.cs @@ -0,0 +1,368 @@ +using Grand.Business.Core.Interfaces.Checkout.Orders; +using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Interfaces.Common.Pdf; +using Grand.Domain.Localization; +using Grand.Domain.Orders; +using Grand.Infrastructure; +using Grand.Web.AdminShared.Controllers; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Orders; +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 BaseOrderControllerTests +{ + // BaseOrderController is abstract; minimal subclass so actions can be invoked directly. + private class TestOrderController( + IOrderViewModelService orderViewModelService, + IOrderService orderService, + ITranslationService translationService, + IContextAccessor contextAccessor, + IPdfService pdfService, + IAdminDataScope scope) + : BaseOrderController(orderViewModelService, orderService, translationService, + contextAccessor, pdfService, scope) + { + public Task<(Order order, IActionResult denied)> LoadAuthorizedOrderPublic(string id) => + LoadAuthorizedOrder(id); + } + + private TestOrderController _controller; + private Mock _orderServiceMock; + private Mock _orderViewModelServiceMock; + private Mock> _scopeMock; + private Mock _pdfServiceMock; + + [TestInitialize] + public void Setup() + { + _orderServiceMock = new Mock(); + _orderViewModelServiceMock = new Mock(); + _scopeMock = new Mock>(); + _scopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); + _pdfServiceMock = new Mock(); + + var translationServiceMock = new Mock(); + translationServiceMock.Setup(t => t.GetResource(It.IsAny())).Returns("resource"); + var contextAccessorMock = new Mock(); + var workContextMock = new Mock(); + workContextMock.Setup(w => w.WorkingLanguage).Returns(new Language { Id = "lang-1" }); + contextAccessorMock.Setup(c => c.WorkContext).Returns(workContextMock.Object); + + _controller = new TestOrderController( + _orderViewModelServiceMock.Object, + _orderServiceMock.Object, + translationServiceMock.Object, + contextAccessorMock.Object, + _pdfServiceMock.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 async Task ListGet_CallsPrepareOrderListModel_WithScopeDefaultStoreId() + { + _scopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); + _orderViewModelServiceMock + .Setup(v => v.PrepareOrderListModel(null, null, null, null, "store-1", null)) + .ReturnsAsync(new OrderListModel()); + + var result = await _controller.List(); + + Assert.IsInstanceOfType(result, typeof(ViewResult)); + _orderViewModelServiceMock.Verify(v => v.PrepareOrderListModel(null, null, null, null, "store-1", null), Times.Once); + } + + [TestMethod] + public async Task ListGet_GlobalScope_PassesEmptyStoreId() + { + _scopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); + _orderViewModelServiceMock + .Setup(v => v.PrepareOrderListModel(null, null, null, null, "", null)) + .ReturnsAsync(new OrderListModel()); + + await _controller.List(); + + _orderViewModelServiceMock.Verify(v => v.PrepareOrderListModel(null, null, null, null, "", null), Times.Once); + } + + [TestMethod] + public async Task ListPost_StoreScope_ForcesModelStoreId() + { + _scopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); + _orderViewModelServiceMock + .Setup(v => v.PrepareOrderModel(It.IsAny(), 1, 10)) + .ReturnsAsync((Enumerable.Empty(), 0)); + + var model = new OrderListModel { StoreId = "attacker-supplied" }; + await _controller.OrderList(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); + _orderViewModelServiceMock + .Setup(v => v.PrepareOrderModel(It.IsAny(), 1, 10)) + .ReturnsAsync((Enumerable.Empty(), 0)); + + var model = new OrderListModel { StoreId = "admin-submitted" }; + await _controller.OrderList(new DataSourceRequest { Page = 1, PageSize = 10 }, model); + + Assert.AreEqual("admin-submitted", model.StoreId); + } + + [TestMethod] + public async Task ListPost_VendorScope_ForcesModelVendorId() + { + _scopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); + _scopeMock.Setup(s => s.DefaultVendorId).Returns("vendor-A"); + _orderViewModelServiceMock + .Setup(v => v.PrepareOrderModel(It.IsAny(), 1, 10)) + .ReturnsAsync((Enumerable.Empty(), 0)); + + var model = new OrderListModel(); + await _controller.OrderList(new DataSourceRequest { Page = 1, PageSize = 10 }, model); + + Assert.AreEqual("vendor-A", model.VendorId); + } + + [TestMethod] + public async Task LoadAuthorizedOrder_NotFound_ReturnsRedirectToList() + { + _orderServiceMock.Setup(s => s.GetOrderById("missing")).ReturnsAsync((Order)null); + + var (order, denied) = await _controller.LoadAuthorizedOrderPublic("missing"); + + Assert.IsNull(order); + var redirect = denied as RedirectToActionResult; + Assert.IsNotNull(redirect); + Assert.AreEqual("List", redirect.ActionName); + _scopeMock.Verify(s => s.HasAccess(It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task LoadAuthorizedOrder_ScopeDenies_ReturnsRedirectToList() + { + var order = new Order { Id = "o1" }; + _orderServiceMock.Setup(s => s.GetOrderById("o1")).ReturnsAsync(order); + _scopeMock.Setup(s => s.HasAccess(order)).ReturnsAsync(false); + + var (resultOrder, denied) = await _controller.LoadAuthorizedOrderPublic("o1"); + + Assert.IsNull(resultOrder); + var redirect = denied as RedirectToActionResult; + Assert.IsNotNull(redirect); + Assert.AreEqual("List", redirect.ActionName); + } + + [TestMethod] + public async Task LoadAuthorizedOrder_ScopeAllows_ReturnsOrderNoDenial() + { + var order = new Order { Id = "o1" }; + _orderServiceMock.Setup(s => s.GetOrderById("o1")).ReturnsAsync(order); + _scopeMock.Setup(s => s.HasAccess(order)).ReturnsAsync(true); + + var (resultOrder, denied) = await _controller.LoadAuthorizedOrderPublic("o1"); + + Assert.AreSame(order, resultOrder); + Assert.IsNull(denied); + } + + [TestMethod] + public async Task EditGet_NotFound_RedirectsToList() + { + _orderServiceMock.Setup(s => s.GetOrderById("missing")).ReturnsAsync((Order)null); + + var result = await _controller.Edit("missing"); + + var redirect = result as RedirectToActionResult; + Assert.IsNotNull(redirect); + Assert.AreEqual("List", redirect.ActionName); + } + + [TestMethod] + public async Task EditGet_Deleted_RedirectsToList() + { + var order = new Order { Id = "o1", Deleted = true }; + _orderServiceMock.Setup(s => s.GetOrderById("o1")).ReturnsAsync(order); + _scopeMock.Setup(s => s.HasAccess(order)).ReturnsAsync(true); + + var result = await _controller.Edit("o1"); + + var redirect = result as RedirectToActionResult; + Assert.IsNotNull(redirect); + Assert.AreEqual("List", redirect.ActionName); + } + + [TestMethod] + public async Task EditGet_ScopeDenies_RedirectsToList() + { + var order = new Order { Id = "o1" }; + _orderServiceMock.Setup(s => s.GetOrderById("o1")).ReturnsAsync(order); + _scopeMock.Setup(s => s.HasAccess(order)).ReturnsAsync(false); + + var result = await _controller.Edit("o1"); + + var redirect = result as RedirectToActionResult; + Assert.IsNotNull(redirect); + Assert.AreEqual("List", redirect.ActionName); + } + + [TestMethod] + public async Task EditGet_Authorized_ReturnsViewAndCallsPrepareOrderDetailsModel() + { + var order = new Order { Id = "o1" }; + _orderServiceMock.Setup(s => s.GetOrderById("o1")).ReturnsAsync(order); + _scopeMock.Setup(s => s.HasAccess(order)).ReturnsAsync(true); + + var result = await _controller.Edit("o1"); + + Assert.IsInstanceOfType(result, typeof(ViewResult)); + _orderViewModelServiceMock.Verify(v => v.PrepareOrderDetailsModel(It.IsAny(), order), Times.Once); + } + + [TestMethod] + public async Task ProductSearchAutoComplete_VendorScope_ForcesVendorIdIntoSearch() + { + _scopeMock.Setup(s => s.DefaultVendorId).Returns("vendor-A"); + // NOTE: brief's test used named-argument Setup/Verify calls (storeId:/vendorId:/keywords:/ + // pageSize:/showHidden:), which don't compile: (1) named arguments inside a Moq Setup + // expression tree must appear in the same relative order as the method's declared parameter + // list - the brief's order (storeId, vendorId, keywords, pageSize, showHidden) puts pageSize + // (declared 3rd) after storeId/vendorId/keywords (declared 7th/8th/18th), which the compiler + // rejects as CS9307 "named argument specification out of position"; and (2) IProductService. + // SearchProducts actually returns Task<(IPagedList, IList)>, not + // Task<(List, int)> as the brief's ReturnsAsync assumed. Rewritten below as a fully + // positional call (all 27 parameters, It.IsAny() for the ones this test doesn't care + // about) to test the same behavior: the controller forces scope.DefaultVendorId into the + // vendorId slot alongside the caller-supplied term/pageSize/showHidden. + var productServiceMock = new Mock(); + var pagedProducts = (Grand.Domain.IPagedList) + new Grand.Domain.PagedList(new List(), 0, 15); + productServiceMock + .Setup(p => p.SearchProducts( + It.IsAny(), It.IsAny(), 15, It.IsAny>(), It.IsAny(), It.IsAny(), + null, "vendor-A", It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), "abc", It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny>(), It.IsAny>(), It.IsAny(), + true, It.IsAny())) + .ReturnsAsync((pagedProducts, (IList)new List())); + + await _controller.ProductSearchAutoComplete("abc", productServiceMock.Object); + + productServiceMock.Verify(p => p.SearchProducts( + It.IsAny(), It.IsAny(), 15, It.IsAny>(), It.IsAny(), It.IsAny(), + null, "vendor-A", It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), "abc", It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny>(), It.IsAny>(), It.IsAny(), + true, It.IsAny()), Times.Once); + } + + [TestMethod] + public async Task PdfInvoice_ThreadsScopeDefaultVendorId() + { + var order = new Order { Id = "o1" }; + _orderServiceMock.Setup(s => s.GetOrderById("o1")).ReturnsAsync(order); + _scopeMock.Setup(s => s.HasAccess(order)).ReturnsAsync(true); + _scopeMock.Setup(s => s.DefaultVendorId).Returns("vendor-A"); + + var result = await _controller.PdfInvoice("o1"); + + Assert.IsInstanceOfType(result, typeof(FileContentResult)); + _pdfServiceMock.Verify( + p => p.PrintOrdersToPdf(It.IsAny(), It.Is>(l => l.Count == 1 && l[0] == order), + "lang-1", "vendor-A"), Times.Once); + } + + [TestMethod] + public async Task PdfInvoiceAll_ThreadsScopeDefaultVendorId() + { + _scopeMock.Setup(s => s.DefaultVendorId).Returns("vendor-A"); + var orders = new List { new() { Id = "o1" } }; + _orderViewModelServiceMock.Setup(v => v.PrepareOrders(It.IsAny())) + .ReturnsAsync((IList)orders); + + var result = await _controller.PdfInvoiceAll(new OrderListModel()); + + Assert.IsInstanceOfType(result, typeof(FileContentResult)); + _pdfServiceMock.Verify( + p => p.PrintOrdersToPdf(It.IsAny(), It.IsAny>(), "lang-1", "vendor-A"), + Times.Once); + } + + [TestMethod] + public async Task PdfInvoiceSelected_ThreadsScopeDefaultVendorId() + { + // Direct regression guard for the final review C2 fix: PdfInvoiceSelected must pass + // scope.DefaultVendorId to IPdfService.PrintOrdersToPdf, matching PdfInvoice/PdfInvoiceAll. + _scopeMock.Setup(s => s.DefaultVendorId).Returns("vendor-A"); + var order = new Order { Id = "o1" }; + _orderServiceMock.Setup(s => s.GetOrdersByIds(It.Is(ids => ids.Length == 1 && ids[0] == "o1"))) + .ReturnsAsync((IList)new List { order }); + _scopeMock.Setup(s => s.HasAccess(order)).ReturnsAsync(true); + + var result = await _controller.PdfInvoiceSelected("o1"); + + Assert.IsInstanceOfType(result, typeof(FileContentResult)); + _pdfServiceMock.Verify( + p => p.PrintOrdersToPdf(It.IsAny(), It.Is>(l => l.Count == 1 && l[0] == order), + "lang-1", "vendor-A"), Times.Once); + } + + [TestMethod] + public async Task PdfInvoiceSelected_ScopeDenies_ExcludesOrderFromPdf() + { + var order = new Order { Id = "o1" }; + _orderServiceMock.Setup(s => s.GetOrdersByIds(It.IsAny())) + .ReturnsAsync((IList)new List { order }); + _scopeMock.Setup(s => s.HasAccess(order)).ReturnsAsync(false); + + var result = await _controller.PdfInvoiceSelected("o1"); + + var redirect = result as RedirectToActionResult; + Assert.IsNotNull(redirect); + Assert.AreEqual("List", redirect.ActionName); + _pdfServiceMock.Verify( + p => p.PrintOrdersToPdf(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny()), + Times.Never); + } + + [TestMethod] + public async Task GoToOrderId_ScopeDenies_RedirectsToList() + { + var order = new Order { Id = "o1" }; + _orderServiceMock.Setup(s => s.GetOrderByNumber(7)).ReturnsAsync(order); + _scopeMock.Setup(s => s.HasAccess(order)).ReturnsAsync(false); + + var result = await _controller.GoToOrderId(new OrderListModel { GoDirectlyToNumber = "7" }); + + var redirect = result as RedirectToActionResult; + Assert.IsNotNull(redirect); + Assert.AreEqual("List", redirect.ActionName); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseOrderManagementControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseOrderManagementControllerTests.cs new file mode 100644 index 000000000..47c714b16 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseOrderManagementControllerTests.cs @@ -0,0 +1,315 @@ +using Grand.Business.Core.Commands.Checkout.Orders; +using Grand.Business.Core.Interfaces.Checkout.Orders; +using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Interfaces.Common.Pdf; +using Grand.Domain.Orders; +using Grand.Infrastructure; +using Grand.Mediator; +using Grand.Web.AdminShared.Controllers; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Orders; +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 BaseOrderManagementControllerTests +{ + private class TestOrderManagementController( + IOrderViewModelService orderViewModelService, + IOrderService orderService, + IOrderStatusService orderStatusService, + ITranslationService translationService, + IContextAccessor contextAccessor, + IPdfService pdfService, + IMediator mediator, + IAdminDataScope scope) + : BaseOrderManagementController(orderViewModelService, orderService, orderStatusService, + translationService, contextAccessor, pdfService, mediator, scope); + + private TestOrderManagementController _controller; + private Mock _orderServiceMock; + private Mock _orderViewModelServiceMock; + private Mock _mediatorMock; + private Mock> _scopeMock; + + [TestInitialize] + public void Setup() + { + _orderServiceMock = new Mock(); + _orderViewModelServiceMock = new Mock(); + _mediatorMock = new Mock(); + _scopeMock = new Mock>(); + + var translationServiceMock = new Mock(); + translationServiceMock.Setup(t => t.GetResource(It.IsAny())).Returns("resource"); + + _controller = new TestOrderManagementController( + _orderViewModelServiceMock.Object, _orderServiceMock.Object, + new Mock().Object, translationServiceMock.Object, + new Mock().Object, new Mock().Object, + _mediatorMock.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 async Task CancelOrder_ScopeDenies_RedirectsToList_NoCommandSent() + { + var order = new Order { Id = "o1" }; + _orderServiceMock.Setup(s => s.GetOrderById("o1")).ReturnsAsync(order); + _scopeMock.Setup(s => s.HasAccess(order)).ReturnsAsync(false); + + var result = await _controller.CancelOrder("o1"); + + Assert.AreEqual("List", (result as RedirectToActionResult)?.ActionName); + _mediatorMock.Verify(m => m.Send(It.IsAny(), default), Times.Never); + } + + [TestMethod] + public async Task CancelOrder_Authorized_SendsCancelCommand_RedirectsToEdit() + { + var order = new Order { Id = "o1" }; + _orderServiceMock.Setup(s => s.GetOrderById("o1")).ReturnsAsync(order); + _scopeMock.Setup(s => s.HasAccess(order)).ReturnsAsync(true); + + var result = await _controller.CancelOrder("o1"); + + var redirect = result as RedirectToActionResult; + Assert.AreEqual("Edit", redirect?.ActionName); + _mediatorMock.Verify(m => m.Send( + It.Is(c => c.Order == order && c.NotifyCustomer), default), Times.Once); + } + + [TestMethod] + public async Task ChangeOrderStatus_ScopeDenies_RedirectsToList_NoUpdate() + { + var order = new Order { Id = "o1" }; + _orderServiceMock.Setup(s => s.GetOrderById("o1")).ReturnsAsync(order); + _scopeMock.Setup(s => s.HasAccess(order)).ReturnsAsync(false); + + var result = await _controller.ChangeOrderStatus("o1", new OrderModel { OrderStatusId = 30 }); + + Assert.AreEqual("List", (result as RedirectToActionResult)?.ActionName); + _orderServiceMock.Verify(s => s.UpdateOrder(It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task Delete_ScopeDenies_RedirectsToList_NoDeleteCommand() + { + var order = new Order { Id = "o1" }; + _orderServiceMock.Setup(s => s.GetOrderById("o1")).ReturnsAsync(order); + _scopeMock.Setup(s => s.HasAccess(order)).ReturnsAsync(false); + + var result = await _controller.Delete(new OrderDeleteModel("o1")); + + Assert.AreEqual("List", (result as RedirectToActionResult)?.ActionName); + _mediatorMock.Verify(m => m.Send(It.IsAny(), default), Times.Never); + } + + [TestMethod] + public async Task Delete_Authorized_ValidModel_SendsDeleteCommand() + { + var order = new Order { Id = "o1" }; + _orderServiceMock.Setup(s => s.GetOrderById("o1")).ReturnsAsync(order); + _scopeMock.Setup(s => s.HasAccess(order)).ReturnsAsync(true); + + var result = await _controller.Delete(new OrderDeleteModel("o1")); + + Assert.AreEqual("List", (result as RedirectToActionResult)?.ActionName); + _mediatorMock.Verify(m => m.Send( + It.Is(c => c.Order == order), default), Times.Once); + } + + [TestMethod] + public async Task EditOrderTotals_ScopeDenies_RedirectsToList_NoUpdate() + { + var order = new Order { Id = "o1" }; + _orderServiceMock.Setup(s => s.GetOrderById("o1")).ReturnsAsync(order); + _scopeMock.Setup(s => s.HasAccess(order)).ReturnsAsync(false); + + var result = await _controller.EditOrderTotals("o1", new OrderModel()); + + Assert.AreEqual("List", (result as RedirectToActionResult)?.ActionName); + _orderServiceMock.Verify(s => s.UpdateOrder(It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task EditOrderTotals_Authorized_UpdatesOrderAndInsertsNote() + { + var order = new Order { Id = "o1" }; + _orderServiceMock.Setup(s => s.GetOrderById("o1")).ReturnsAsync(order); + _scopeMock.Setup(s => s.HasAccess(order)).ReturnsAsync(true); + + var model = new OrderModel { OrderTotalValue = 99.0, CurrencyRate = 1.0 }; + var result = await _controller.EditOrderTotals("o1", model); + + Assert.AreEqual("o1", (result as RedirectToActionResult)?.RouteValues["id"]); + Assert.AreEqual(99.0, order.OrderTotal); + _orderServiceMock.Verify(s => s.InsertOrderNote(It.Is(n => n.Note == "Order totals have been edited")), Times.Once); + } + + [TestMethod] + public async Task SaveOrderItem_ScopeDenies_RedirectsToList_NoCommandSent() + { + var order = new Order { Id = "o1" }; + _orderServiceMock.Setup(s => s.GetOrderById("o1")).ReturnsAsync(order); + _scopeMock.Setup(s => s.HasAccess(order)).ReturnsAsync(false); + + var result = await _controller.SaveOrderItem("o1", new OrderItemsModel(new List(), "i1")); + + Assert.AreEqual("List", (result as RedirectToActionResult)?.ActionName); + _mediatorMock.Verify(m => m.Send(It.IsAny(), default), Times.Never); + } + + [TestMethod] + public async Task SaveOrderItem_OrderCancelled_ErrorsWithoutSendingCommand() + { + var order = new Order { Id = "o1", OrderStatusId = (int)OrderStatusSystem.Cancelled }; + _orderServiceMock.Setup(s => s.GetOrderById("o1")).ReturnsAsync(order); + _scopeMock.Setup(s => s.HasAccess(order)).ReturnsAsync(true); + + var result = await _controller.SaveOrderItem("o1", new OrderItemsModel(new List(), "i1")); + + Assert.AreEqual("Edit", (result as RedirectToActionResult)?.ActionName); + _mediatorMock.Verify(m => m.Send(It.IsAny(), default), Times.Never); + } + + [TestMethod] + public async Task DeleteOrderItem_ScopeDenies_RedirectsToList_NoCommandSent() + { + var order = new Order { Id = "o1" }; + _orderServiceMock.Setup(s => s.GetOrderById("o1")).ReturnsAsync(order); + _scopeMock.Setup(s => s.HasAccess(order)).ReturnsAsync(false); + + var result = await _controller.DeleteOrderItem("o1", "i1"); + + Assert.AreEqual("List", (result as RedirectToActionResult)?.ActionName); + _mediatorMock.Verify(m => m.Send(It.IsAny(), default), Times.Never); + } + + [TestMethod] + public async Task UploadLicenseFilePopupGet_ScopeDenies_RedirectsToList() + { + var order = new Order { Id = "o1" }; + _orderServiceMock.Setup(s => s.GetOrderById("o1")).ReturnsAsync(order); + _scopeMock.Setup(s => s.HasAccess(order)).ReturnsAsync(false); + var productServiceMock = new Mock(); + + var result = await _controller.UploadLicenseFilePopup("o1", "i1", productServiceMock.Object); + + Assert.AreEqual("List", (result as RedirectToActionResult)?.ActionName); + } + + [TestMethod] + public async Task DeleteLicenseFilePopup_Authorized_ClearsLicenseAndRedirectsToEdit() + { + var orderItem = new OrderItem { Id = "i1", LicenseDownloadId = "dl-1" }; + var order = new Order { Id = "o1" }; + order.OrderItems.Add(orderItem); + _orderServiceMock.Setup(s => s.GetOrderById("o1")).ReturnsAsync(order); + _scopeMock.Setup(s => s.HasAccess(order)).ReturnsAsync(true); + + var result = await _controller.DeleteLicenseFilePopup( + new OrderModel.UploadLicenseModel { OrderId = "o1", OrderItemId = "i1" }); + + Assert.IsNull(orderItem.LicenseDownloadId); + Assert.AreEqual("Edit", (result as RedirectToActionResult)?.ActionName); + } + + [TestMethod] + public async Task AddProductToOrder_ScopeDenies_RedirectsToList() + { + var order = new Order { Id = "o1" }; + _orderServiceMock.Setup(s => s.GetOrderById("o1")).ReturnsAsync(order); + _scopeMock.Setup(s => s.HasAccess(order)).ReturnsAsync(false); + + var result = await _controller.AddProductToOrder("o1"); + + Assert.AreEqual("List", (result as RedirectToActionResult)?.ActionName); + } + + [TestMethod] + public async Task AddProductToOrderDetailsPost_NoWarnings_RedirectsToEdit() + { + var order = new Order { Id = "o1" }; + _orderServiceMock.Setup(s => s.GetOrderById("o1")).ReturnsAsync(order); + _scopeMock.Setup(s => s.HasAccess(order)).ReturnsAsync(true); + _orderViewModelServiceMock + .Setup(v => v.AddProductToOrderDetails(It.IsAny())) + .ReturnsAsync(new List()); + + var result = await _controller.AddProductToOrderDetails( + new AddProductToOrderModel("o1", "p1", 0, 0, 1, 0)); + + var redirect = result as RedirectToActionResult; + Assert.AreEqual("Edit", redirect?.ActionName); + Assert.AreEqual("o1", redirect?.RouteValues["id"]); + } + + [TestMethod] + public async Task AddressEditGet_ScopeDenies_RedirectsToList() + { + var order = new Order { Id = "o1", BillingAddress = new Grand.Domain.Common.Address { Id = "a1" } }; + _orderServiceMock.Setup(s => s.GetOrderById("o1")).ReturnsAsync(order); + _scopeMock.Setup(s => s.HasAccess(order)).ReturnsAsync(false); + + var result = await _controller.AddressEdit("a1", "o1", true); + + Assert.AreEqual("List", (result as RedirectToActionResult)?.ActionName); + } + + [TestMethod] + public async Task OrderNotesSelect_NotFound_ThrowsArgumentException() + { + _orderServiceMock.Setup(s => s.GetOrderById("missing")).ReturnsAsync((Order)null); + + await Assert.ThrowsExactlyAsync( + () => _controller.OrderNotesSelect("missing", new Grand.Web.Common.DataSource.DataSourceRequest())); + } + + [TestMethod] + public async Task OrderNotesSelect_ScopeDenies_ReturnsEmptyContent_NoThrow() + { + var order = new Order { Id = "o1" }; + _orderServiceMock.Setup(s => s.GetOrderById("o1")).ReturnsAsync(order); + _scopeMock.Setup(s => s.HasAccess(order)).ReturnsAsync(false); + + var result = await _controller.OrderNotesSelect("o1", new Grand.Web.Common.DataSource.DataSourceRequest()); + + var content = result as ContentResult; + Assert.IsNotNull(content); + Assert.AreEqual("", content.Content); + } + + [TestMethod] + public async Task OrderNoteAdd_ScopeDenies_ReturnsJsonResultFalse() + { + var order = new Order { Id = "o1" }; + _orderServiceMock.Setup(s => s.GetOrderById("o1")).ReturnsAsync(order); + _scopeMock.Setup(s => s.HasAccess(order)).ReturnsAsync(false); + + var result = await _controller.OrderNoteAdd("o1", null, false, "msg"); + + var json = result as JsonResult; + Assert.IsNotNull(json); + Assert.IsFalse((bool)json.Value.GetType().GetProperty("Result").GetValue(json.Value)); + _orderViewModelServiceMock.Verify(v => v.InsertOrderNote(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/OrderControllerRoutingTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/OrderControllerRoutingTests.cs new file mode 100644 index 000000000..3da4aebf3 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/OrderControllerRoutingTests.cs @@ -0,0 +1,30 @@ +using Grand.Web.Admin.Controllers; +using Grand.Web.AdminShared.Controllers; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Grand.Web.Admin.Tests.Controllers; + +[TestClass] +public class OrderControllerRoutingTests +{ + [TestMethod] + public void AdminOrderController_InheritsBaseOrderManagementController() => + Assert.IsTrue(typeof(BaseOrderManagementController).IsAssignableFrom(typeof(OrderController))); + + [TestMethod] + public void AdminOrderController_HasAutoValidateAntiforgeryToken() => + Assert.IsTrue(typeof(OrderController) + .GetCustomAttributes(typeof(Microsoft.AspNetCore.Mvc.AutoValidateAntiforgeryTokenAttribute), false) + .Length > 0); + + [TestMethod] + public void AdminOrderController_DeclaresExportAndDeleteSelectedItself() + { + var declared = typeof(OrderController) + .GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.DeclaredOnly) + .Select(m => m.Name).ToHashSet(); + Assert.IsTrue(declared.Contains("ExportExcelAll")); + Assert.IsTrue(declared.Contains("ExportExcelSelected")); + Assert.IsTrue(declared.Contains("DeleteSelected")); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/RoutedOrderDataScopeTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/RoutedOrderDataScopeTests.cs new file mode 100644 index 000000000..82a47c649 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/RoutedOrderDataScopeTests.cs @@ -0,0 +1,95 @@ +#nullable enable + +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Domain.Customers; +using Grand.Domain.Orders; +using Grand.Domain.Vendors; +using Grand.Infrastructure; +using Grand.Web.AdminShared.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Admin.Tests.Controllers; + +/// +/// Covers the routing decision itself - which concrete IAdminDataScope<Order> the resolver +/// delegates to for each area, and that an unrecognized or missing area fails closed rather than +/// falling back to any concrete scope. Mirrors RoutedProductDataScopeTests: real concrete scopes +/// built with mocked IContextAccessor/IGroupService dependencies, since AdminOrderDataScope/ +/// StoreOrderDataScope/VendorOrderDataScope have no virtual members for Moq to intercept. +/// +[TestClass] +public class RoutedOrderDataScopeTests +{ + private const string StaffStoreId = "store-1"; + private const string VendorId = "vendor-1"; + + private AdminOrderDataScope _adminScope = null!; + private StoreOrderDataScope _storeScope = null!; + private VendorOrderDataScope _vendorScope = null!; + + [TestInitialize] + public void Setup() + { + var workContext = new Mock(); + workContext.Setup(x => x.CurrentCustomer).Returns(new Customer { StaffStoreId = StaffStoreId }); + workContext.Setup(x => x.CurrentVendor).Returns(new Vendor { Id = VendorId }); + var contextAccessor = new Mock(); + contextAccessor.Setup(x => x.WorkContext).Returns(workContext.Object); + + var groupService = new Mock(); + groupService.Setup(g => g.IsSalesManager(It.IsAny())).ReturnsAsync(false); + + _adminScope = new AdminOrderDataScope(contextAccessor.Object, groupService.Object); + _storeScope = new StoreOrderDataScope(contextAccessor.Object); + _vendorScope = new VendorOrderDataScope(contextAccessor.Object); + } + + private RoutedOrderDataScope ResolverForArea(string? area) + { + var httpContext = new DefaultHttpContext(); + if (area is not null) httpContext.Request.RouteValues["area"] = area; + var httpContextAccessor = new Mock(); + httpContextAccessor.Setup(x => x.HttpContext).Returns(httpContext); + return new RoutedOrderDataScope(httpContextAccessor.Object, _adminScope, _storeScope, _vendorScope); + } + + [TestMethod] + public void AdminArea_ResolvesToAdminScope() + { + var resolver = ResolverForArea("Admin"); + Assert.AreEqual("Admin", resolver.ResourceKeyPrefix); + Assert.IsNull(resolver.DefaultStoreId); + Assert.IsNull(resolver.DefaultVendorId); + Assert.IsTrue(resolver.CanFeatureOnHomepage); + } + + [TestMethod] + public void StoreArea_ResolvesToStoreScope() + { + var resolver = ResolverForArea("Store"); + Assert.AreEqual(StaffStoreId, resolver.DefaultStoreId); + Assert.IsNull(resolver.DefaultVendorId); + } + + [TestMethod] + public void VendorArea_ResolvesToVendorScope() + { + var resolver = ResolverForArea("Vendor"); + Assert.AreEqual("Vendor", resolver.ResourceKeyPrefix); + Assert.AreEqual(VendorId, resolver.DefaultVendorId); + Assert.IsFalse(resolver.ShowStoreSelector); + Assert.IsFalse(resolver.CanFeatureOnHomepage); + } + + [TestMethod] + public void UnrecognizedOrMissingArea_ThrowsFailClosed() + { + var resolver = ResolverForArea("Vue"); + Assert.Throws(() => _ = resolver.ResourceKeyPrefix); + + var resolverNoArea = ResolverForArea(null); + Assert.Throws(() => _ = resolverNoArea.ResourceKeyPrefix); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/StoreOrderDataScopeTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/StoreOrderDataScopeTests.cs new file mode 100644 index 000000000..45255104b --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/StoreOrderDataScopeTests.cs @@ -0,0 +1,51 @@ +using Grand.Domain.Customers; +using Grand.Domain.Orders; +using Grand.Infrastructure; +using Grand.Web.AdminShared.Services; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Admin.Tests.Controllers; + +[TestClass] +public class StoreOrderDataScopeTests +{ + private static StoreOrderDataScope 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 StoreOrderDataScope(contextAccessorMock.Object); + } + + [TestMethod] + public async Task HasAccess_MatchingStoreId_True() + { + var scope = Build("store-1"); + Assert.IsTrue(await scope.HasAccess(new Order { StoreId = "store-1" })); + } + + [TestMethod] + public async Task HasAccess_MismatchedStoreId_False() + { + var scope = Build("store-1"); + Assert.IsFalse(await scope.HasAccess(new Order { StoreId = "store-2" })); + } + + [TestMethod] + public async Task HasAccess_NullEntity_False() + { + var scope = Build("store-1"); + Assert.IsFalse(await scope.HasAccess(null)); + } + + [TestMethod] + public void DefaultStoreId_ReturnsStaffStoreId() + { + var scope = Build("store-1"); + Assert.AreEqual("store-1", scope.DefaultStoreId); + Assert.IsNull(scope.DefaultVendorId); + } +} diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/VendorOrderDataScopeTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/VendorOrderDataScopeTests.cs new file mode 100644 index 000000000..bce658a48 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/VendorOrderDataScopeTests.cs @@ -0,0 +1,65 @@ +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 VendorOrderDataScopeTests +{ + private static VendorOrderDataScope 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 VendorOrderDataScope(contextAccessorMock.Object); + } + + [TestMethod] + public async Task HasAccess_AnyItemMatchesVendor_True() + { + var scope = Build("vendor-A"); + var order = new Order(); + order.OrderItems.Add(new OrderItem { VendorId = "vendor-B" }); + order.OrderItems.Add(new OrderItem { VendorId = "vendor-A" }); + + Assert.IsTrue(await scope.HasAccess(order)); + } + + [TestMethod] + public async Task HasAccess_NoItemMatchesVendor_False() + { + var scope = Build("vendor-A"); + var order = new Order(); + order.OrderItems.Add(new OrderItem { VendorId = "vendor-B" }); + + Assert.IsFalse(await scope.HasAccess(order)); + } + + [TestMethod] + public void FilterOrderItems_MixedVendorOrder_ReturnsOnlyOwnItems() + { + var scope = Build("vendor-A"); + var itemA1 = new OrderItem { Id = "i1", VendorId = "vendor-A" }; + var itemB = new OrderItem { Id = "i2", VendorId = "vendor-B" }; + var itemA2 = new OrderItem { Id = "i3", VendorId = "vendor-A" }; + + var filtered = scope.FilterOrderItems([itemA1, itemB, itemA2]).ToList(); + + CollectionAssert.AreEqual(new[] { itemA1, itemA2 }, filtered); + } + + [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); + } +} diff --git a/src/Tests/Grand.Web.Store.Tests/Controllers/OrderControllerRoutingTests.cs b/src/Tests/Grand.Web.Store.Tests/Controllers/OrderControllerRoutingTests.cs new file mode 100644 index 000000000..3edba26bf --- /dev/null +++ b/src/Tests/Grand.Web.Store.Tests/Controllers/OrderControllerRoutingTests.cs @@ -0,0 +1,50 @@ +using Grand.Web.AdminShared.Controllers; +using Grand.Web.Common.Filters; +using Grand.Web.Store.Controllers; +using Grand.Web.Store.Extensions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Grand.Web.Store.Tests.Controllers; + +[TestClass] +public class StoreControllerRoutingTests +{ + [TestMethod] + public void StoreOrderController_InheritsBaseOrderManagementController() => + Assert.IsTrue(typeof(BaseOrderManagementController).IsAssignableFrom(typeof(OrderController))); + + [TestMethod] + public void StoreOrderController_HasAutoValidateAntiforgeryToken() => + Assert.IsTrue(typeof(OrderController) + .GetCustomAttributes(typeof(Microsoft.AspNetCore.Mvc.AutoValidateAntiforgeryTokenAttribute), false) + .Length > 0); + + // Regression guard for the defect class Task 17 caught: BaseOrderManagementController can't + // carry a host's [Area]/[Authorize*] attributes itself (they differ per host), so each concrete + // subclass must restate its own - a missing one here would 404 or deauthorize the whole + // controller silently. Same shape as ProductControllerAttributesTests (Admin). + [TestMethod] + public void StoreOrderController_HasAreaAttributeWithStoreArea() + { + var areaAttr = (AreaAttribute)Attribute.GetCustomAttribute(typeof(OrderController), typeof(AreaAttribute), false); + Assert.IsNotNull(areaAttr, "Missing [Area]."); + Assert.AreEqual(Constants.AreaStore, areaAttr.RouteValue); + } + + [TestMethod] + public void StoreOrderController_HasAuthorizeStoreAttribute() => + Assert.IsTrue(typeof(OrderController).IsDefined(typeof(AuthorizeStoreAttribute), false), + "Missing [AuthorizeStore]."); + + [TestMethod] + public void StoreOrderController_DoesNotDeclareAdminOnlyExportOrDeleteSelected() + { + var declared = typeof(OrderController) + .GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.DeclaredOnly) + .Select(m => m.Name).ToHashSet(); + Assert.IsFalse(declared.Contains("ExportExcelAll")); + Assert.IsFalse(declared.Contains("ExportExcelSelected")); + Assert.IsFalse(declared.Contains("DeleteSelected")); + } +} diff --git a/src/Tests/Grand.Web.Vendor.Tests/Controllers/OrderControllerSurfaceTests.cs b/src/Tests/Grand.Web.Vendor.Tests/Controllers/OrderControllerSurfaceTests.cs new file mode 100644 index 000000000..d68f717d5 --- /dev/null +++ b/src/Tests/Grand.Web.Vendor.Tests/Controllers/OrderControllerSurfaceTests.cs @@ -0,0 +1,59 @@ +using Grand.Web.Common.Filters; +using Grand.Web.Vendor.Controllers; +using Grand.Web.Vendor.Extensions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Grand.Web.Vendor.Tests.Controllers; + +[TestClass] +public class OrderControllerSurfaceTests +{ + // Regression guard for ARCH-001 Order consolidation spec §3.5: Vendor's OrderController must + // inherit BaseOrderController directly, never BaseOrderManagementController, so no mutating + // action method exists on its type at all - not permission-gated, genuinely absent. + private static readonly string[] ManagementOnlyActionNames = [ + "CancelOrder", "SaveOrderTags", "ChangeOrderStatus", "Delete", "EditOrderTotals", + "EditShippingMethod", "EditUserFields", "SaveOrderItem", "DeleteOrderItem", + "CancelOrderItem", "ResetDownloadCount", "ActivateDownloadItem", "UploadLicenseFilePopup", + "DeleteLicenseFilePopup", "AddProductToOrder", "AddProductToOrderDetails", "AddressEdit", + "OrderNotesSelect", "OrderNoteAdd", "OrderNoteDelete", "ExportExcelAll", + "ExportExcelSelected", "DeleteSelected" + ]; + + [TestMethod] + public void VendorOrderController_DoesNotInheritBaseOrderManagementController() + { + Assert.IsFalse(typeof(Grand.Web.AdminShared.Controllers.BaseOrderManagementController) + .IsAssignableFrom(typeof(OrderController))); + } + + [TestMethod] + public void VendorOrderController_HasNoManagementOnlyActionMethods() + { + var declaredMethodNames = typeof(OrderController) + .GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance) + .Select(m => m.Name) + .ToHashSet(); + + var leaked = ManagementOnlyActionNames.Where(declaredMethodNames.Contains).ToList(); + Assert.AreEqual(0, leaked.Count, $"Vendor's OrderController exposes management-only action(s): {string.Join(", ", leaked)}"); + } + + // Regression guard for the defect class Task 17 caught: BaseOrderController can't carry a + // host's [Area]/[Authorize*] attributes itself (they differ per host), so each concrete + // subclass must restate its own - a missing one here would 404 or deauthorize the whole + // controller silently. Same shape as ProductControllerAttributesTests (Admin). + [TestMethod] + public void VendorOrderController_HasAreaAttributeWithVendorArea() + { + var areaAttr = (AreaAttribute)Attribute.GetCustomAttribute(typeof(OrderController), typeof(AreaAttribute), false); + Assert.IsNotNull(areaAttr, "Missing [Area]."); + Assert.AreEqual(Constants.AreaVendor, areaAttr.RouteValue); + } + + [TestMethod] + public void VendorOrderController_HasAuthorizeVendorAttribute() => + Assert.IsTrue(typeof(OrderController).IsDefined(typeof(AuthorizeVendorAttribute), false), + "Missing [AuthorizeVendor]."); +} diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/AddProductToOrderDetails.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/AddProductToOrderDetails.cshtml deleted file mode 100644 index 17a070d97..000000000 --- a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/AddProductToOrderDetails.cshtml +++ /dev/null @@ -1,111 +0,0 @@ -@model OrderModel.AddOrderProductModel.ProductDetailsModel -@{ - ViewBag.Title = string.Format(Loc["Admin.Orders.Products.AddNew.Title2"], Model.Name, Model.OrderNumber); -} -
-
- -
-
-
-
-
- - @string.Format(Loc["Admin.Orders.Products.AddNew.Title2"], Model.Name, Model.OrderNumber) - @Html.ActionLink("(" + Loc["Admin.Orders.Products.AddNew.BackToList"] + ")", "AddProductToOrder", new { orderId = Model.OrderId }) -
-
-
-
- @if (Model.Warnings.Count > 0) - { -
- @foreach (var warning in Model.Warnings) - { - @warning -
- } -
- } -
- @if (Model.ProductType == ProductType.SimpleProduct) - { -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
- @{ - var dataDict = new ViewDataDictionary(ViewData) { new("productId", Model.ProductId) }; - - } - @{ - var dataDictGiftVoucher = new ViewDataDictionary(ViewData) - { - TemplateInfo = - { - HtmlFieldPrefix = "giftvoucher" - } - }; - - } -
-
-
- -
-
-
-
-
- } - else if (Model.ProductType == ProductType.GroupedProduct) - { -
-
- Grouped products are not currently supported for adding to an existing order -
-
- } - else if (Model.ProductType == ProductType.Reservation) - { -
-
- Reservation products are not currently supported for adding to an existing order -
-
- } - else - { -
- This product type (unknown) is not currently supported for adding to an existing order -
- } -
-
-
-
- - \ No newline at end of file diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/AddressEdit.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/AddressEdit.cshtml deleted file mode 100644 index a1b8c29ff..000000000 --- a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/AddressEdit.cshtml +++ /dev/null @@ -1,36 +0,0 @@ -@model OrderAddressModel -@{ - //page title - ViewBag.Title = Loc["Admin.Orders.Address.EditAddress"]; -} -
- -
-
-
-
-
- - @Loc["Admin.Orders.Address.EditAddress"] - - - @Html.ActionLink(Loc["Admin.Orders.Address.BackToOrder"], "Edit", new { id = Model.OrderId }) - -
-
- - -
-
-
- -
-
-
-
-
\ No newline at end of file diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Edit.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Edit.cshtml deleted file mode 100644 index 8695293df..000000000 --- a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Edit.cshtml +++ /dev/null @@ -1,133 +0,0 @@ -@using Grand.Business.Core.Interfaces.Common.Security -@using Grand.Domain.Permissions -@model OrderModel -@inject IPermissionService permissionService -@{ - //page title - ViewBag.Title = Loc["Admin.Orders.EditOrderDetails"]; - //has "Manage Documents" permission? - var canManageDocuments = await permissionService.Authorize(StandardPermission.ManageDocuments); - var canManageMessageQueue = await permissionService.Authorize(StandardPermission.ManageMessageQueue); -} -
- -
-
-
-
-
-
- - @Loc["Admin.Orders.EditOrderDetails"] - @Model.OrderNumber - - @Html.ActionLink(Loc["Admin.Orders.BackToList"], "List") - -
-
-
- - @Loc["Admin.Orders.PdfInvoice"] - - - @Loc["Admin.Common.Delete"] - - - - @Loc["Admin.Common.Cancel"] - - - - -
-
-
-
- - - - -
- -
-
-
- - - -
- -
-
-
- @if (Model.IsShippable) - { - - -
- -
-
-
- } - - -
- -
-
-
- - -
- -
-
-
- @if (canManageDocuments) - { - - -
- -
-
-
- } - - -
-
- -
-
- -
-
-
-
- @if (canManageMessageQueue) - { - - -
- -
-
-
- } - -
-
-
-
-
-
-
- \ No newline at end of file diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/CreateOrUpdateAddress.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/CreateOrUpdateAddress.cshtml deleted file mode 100644 index 02aab8b36..000000000 --- a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/CreateOrUpdateAddress.cshtml +++ /dev/null @@ -1,5 +0,0 @@ -@model OrderAddressModel - -
- - \ No newline at end of file diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/OrderDetails.Notes.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/OrderDetails.Notes.cshtml deleted file mode 100644 index 5d5ee809d..000000000 --- a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/OrderDetails.Notes.cshtml +++ /dev/null @@ -1,205 +0,0 @@ -@using Grand.Domain.Media -@model OrderModel -@{ - ViewData["DownloadType"] = DownloadType.Order; - ViewData["ReferenceId"] = Model.Id; -} -
- -
-
-
- -
- -

- - @Loc["Admin.Orders.OrderNotes.AddTitle"] - -

- - -
-
-
- -
- - -
-
-
- -
- -
- - -
-
-
-
- -
- - -
-
-
-
- -
-
-
-
- - \ No newline at end of file diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/OrderDetails.Products.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/OrderDetails.Products.cshtml deleted file mode 100644 index b2bac2cf8..000000000 --- a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/OrderDetails.Products.cshtml +++ /dev/null @@ -1,355 +0,0 @@ -@using Grand.Domain.Tax -@model OrderModel - - -
- - - - -
- @foreach (var item in Model.Items) - { - - - - } - - - - @if (Model.HasDownloadableProducts) - { - - } - - - - - - - - - - - - @if (Model.Items.Sum(x => x.DiscountInclTaxValue) > 0) - { - - } - @if (Model.Items.Sum(x => x.CommissionValue) > 0) - { - - } - - - - - - @for (var j = 0; j < Model.Items.Count; j++) - { - var item = Model.Items[j]; - - - - - - - @if (Model.Items.Sum(x => x.DiscountInclTaxValue) > 0) - { - - } - @if (Model.Items.Sum(x => x.CommissionValue) > 0) - { - - } - - - - - } - -
- @Loc["Admin.Orders.Products.Picture"] - - @Loc["Admin.Orders.Products.ProductName"] - - @Loc["Admin.Orders.Products.Price"] - - @Loc["Admin.Orders.Products.Quantity"] - - @Loc["Admin.Orders.Products.Discount"] - - @Loc["Admin.Orders.Products.Commission"] - - @Loc["Admin.Orders.Products.Total"] - - @Loc["Admin.Common.Edit"] -
- - -
- - @item.ProductName - - @if (!string.IsNullOrEmpty(item.AttributeInfo)) - { -

- @Html.Raw(item.AttributeInfo) -

- } - @if (!string.IsNullOrEmpty(item.RecurringInfo)) - { -

- @Html.Raw(item.RecurringInfo) -

- } - @if (!string.IsNullOrEmpty(item.RentalInfo)) - { -

- @Html.Raw(item.RentalInfo) -

- } - @if (!string.IsNullOrEmpty(item.Sku)) - { -

- @Loc["Admin.Orders.Products.SKU"]: - @item.Sku -

- } - @if (!string.IsNullOrEmpty(item.VendorName)) - { -

- @Loc["Admin.Orders.Products.Vendor"]: - @item.VendorName -

- } - @if (item.MerchandiseReturnIds.Count > 0) - { -

- @Loc["Admin.Orders.Products.MerchandiseReturns"]: - @for (var i = 0; i < item.MerchandiseReturnIds.Count; i++) - { -  @Loc["Admin.Orders.MerchandiseReturns.View"] - if (i != item.MerchandiseReturnIds.Count - 1) - { - , - } - } -

- } - @if (item.PurchasedGiftVoucherIds.Count > 0) - { -

- @Loc["Admin.Orders.Products.GiftVouchers"]: - @for (var i = 0; i < item.PurchasedGiftVoucherIds.Count; i++) - { -  @Loc["Admin.Orders.Products.GiftVoucher.View"] - if (i != item.PurchasedGiftVoucherIds.Count - 1) - { - , - } - } -

- } - @if (Model.HasDownloadableProducts) - { -

- @if (item.IsDownload) - { -

- @string.Format(Loc["Admin.Orders.Products.Download.DownloadCount"], item.DownloadCount) - -
-
- if (item.DownloadActivationType == DownloadActivationType.Manually) - { -
- -
-
- } - -
- - @Loc["Admin.Orders.Products.License"] - -
- @if (item.LicenseDownloadGuid != Guid.Empty) - { - @Loc["Admin.Orders.Products.License.DownloadLicense"] - } -
- - @Loc["Admin.Orders.Products.License.UploadFile"] - -
- - } -

- } -
-
- @switch (Model.TaxDisplayType) - { - case TaxDisplayType.ExcludingTax: - { - @item.UnitPriceExclTax - } - break; - case TaxDisplayType.IncludingTax: - { - @item.UnitPriceInclTax - } - break; - } - -
- - - - - -
- @Loc["Admin.Orders.Products.Edit.ExclTax"] - - @* - - *@ - -
-
-
- @item.Quantity -
- - - - -
- @* - - *@ - -
-
- @if (item.OpenQty > 0) - { -
- Open: @item.OpenQty -
- } - @if (item.CancelQty > 0) - { -
- Canceled: @item.CancelQty -
- } -
- @switch (Model.TaxDisplayType) - { - case TaxDisplayType.ExcludingTax: - { - @item.DiscountExclTax - } - break; - case TaxDisplayType.IncludingTax: - { - @item.DiscountInclTax - } - break; - } - - -
@item.Commission
-
- - @switch (Model.TaxDisplayType) - { - case TaxDisplayType.ExcludingTax: - { - @item.SubTotalExclTax - } - break; - case TaxDisplayType.IncludingTax: - { - @item.SubTotalInclTax - } - break; - } - - -
- - - - - -
-
-
-
- - -@if (!string.IsNullOrEmpty(Model.CheckoutAttributeInfo)) -{ -
- @Html.Raw(Model.CheckoutAttributeInfo) -
-} -
- -
\ No newline at end of file diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/OrderDetails.Shipment.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/OrderDetails.Shipment.cshtml deleted file mode 100644 index 619073d3d..000000000 --- a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/OrderDetails.Shipment.cshtml +++ /dev/null @@ -1,167 +0,0 @@ -@using Grand.Business.Core.Interfaces.Common.Security -@using Grand.Domain.Permissions -@model OrderModel -@inject IPermissionService permissionService; - -@if (Model.IsShippable) -{ - @if (await permissionService.Authorize(PermissionSystemName.Shipments)) - { -
-
-

@Loc["Admin.Orders.Shipments"]

-
-
- -
-
-
- - -
-
-
-
- - - - } -} -else -{ -
-
@Loc["Admin.Orders.ShippingInfo.NotRequired"]
-
-} - - \ No newline at end of file diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.AddProducts.Bottom.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.AddProducts.Bottom.cshtml new file mode 100644 index 000000000..1a26b9069 --- /dev/null +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.AddProducts.Bottom.cshtml @@ -0,0 +1,2 @@ +@model OrderModel.AddOrderProductModel + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.AddProducts.Top.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.AddProducts.Top.cshtml new file mode 100644 index 000000000..8bd2cd330 --- /dev/null +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.AddProducts.Top.cshtml @@ -0,0 +1,2 @@ +@model OrderModel.AddOrderProductModel + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.AddProductsDetails.Bottom.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.AddProductsDetails.Bottom.cshtml new file mode 100644 index 000000000..0c9c94f10 --- /dev/null +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.AddProductsDetails.Bottom.cshtml @@ -0,0 +1,2 @@ +@model OrderModel.AddOrderProductModel.ProductDetailsModel + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.AddProductsDetails.Top.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.AddProductsDetails.Top.cshtml new file mode 100644 index 000000000..752daacdb --- /dev/null +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.AddProductsDetails.Top.cshtml @@ -0,0 +1,2 @@ +@model OrderModel.AddOrderProductModel.ProductDetailsModel + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.AddressDetails.Bottom.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.AddressDetails.Bottom.cshtml new file mode 100644 index 000000000..93bdf1e23 --- /dev/null +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.AddressDetails.Bottom.cshtml @@ -0,0 +1,2 @@ +@model OrderAddressModel + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.AddressDetails.Top.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.AddressDetails.Top.cshtml new file mode 100644 index 000000000..7b8c2aec6 --- /dev/null +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.AddressDetails.Top.cshtml @@ -0,0 +1,2 @@ +@model OrderAddressModel + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.AddressEditButtons.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.AddressEditButtons.cshtml new file mode 100644 index 000000000..8ff6481bf --- /dev/null +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.AddressEditButtons.cshtml @@ -0,0 +1,2 @@ +@model OrderAddressModel + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Addresses.Bottom.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Addresses.Bottom.cshtml new file mode 100644 index 000000000..e4ee34fc0 --- /dev/null +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Addresses.Bottom.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Addresses.Top.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Addresses.Top.cshtml new file mode 100644 index 000000000..40d452ae9 --- /dev/null +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Addresses.Top.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.DetailsButtons.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.DetailsButtons.cshtml new file mode 100644 index 000000000..bdb854259 --- /dev/null +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.DetailsButtons.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.DetailsTabs.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.DetailsTabs.cshtml new file mode 100644 index 000000000..add3da962 --- /dev/null +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.DetailsTabs.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Documents.Bottom.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Documents.Bottom.cshtml new file mode 100644 index 000000000..fddb1e590 --- /dev/null +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Documents.Bottom.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Documents.Top.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Documents.Top.cshtml new file mode 100644 index 000000000..2cb7444b9 --- /dev/null +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Documents.Top.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Info.Bottom.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Info.Bottom.cshtml new file mode 100644 index 000000000..70d6d33b7 --- /dev/null +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Info.Bottom.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Info.Top.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Info.Top.cshtml new file mode 100644 index 000000000..a3b52af3f --- /dev/null +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Info.Top.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Notes.Bottom.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Notes.Bottom.cshtml new file mode 100644 index 000000000..b7ef13c8d --- /dev/null +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Notes.Bottom.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Notes.Top.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Notes.Top.cshtml new file mode 100644 index 000000000..f82a97e62 --- /dev/null +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Notes.Top.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Notifications.Bottom.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Notifications.Bottom.cshtml new file mode 100644 index 000000000..94e49af76 --- /dev/null +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Notifications.Bottom.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Notifications.Top.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Notifications.Top.cshtml new file mode 100644 index 000000000..299d67f6f --- /dev/null +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Notifications.Top.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Products.Bottom.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Products.Bottom.cshtml new file mode 100644 index 000000000..db034bb90 --- /dev/null +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Products.Bottom.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Products.Top.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Products.Top.cshtml new file mode 100644 index 000000000..2344e98ba --- /dev/null +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Products.Top.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Shipment.AddressBottom.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Shipment.AddressBottom.cshtml new file mode 100644 index 000000000..e4ee34fc0 --- /dev/null +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Shipment.AddressBottom.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Shipment.AddressTop.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Shipment.AddressTop.cshtml new file mode 100644 index 000000000..40d452ae9 --- /dev/null +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Shipment.AddressTop.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Shipment.ShippingBottom.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Shipment.ShippingBottom.cshtml new file mode 100644 index 000000000..a2f07ec50 --- /dev/null +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Shipment.ShippingBottom.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Shipment.ShippingTop.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Shipment.ShippingTop.cshtml new file mode 100644 index 000000000..c7d90b715 --- /dev/null +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.Shipment.ShippingTop.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.UploadLicenseButtons.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.UploadLicenseButtons.cshtml new file mode 100644 index 000000000..7cfda5363 --- /dev/null +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/WidgetZone.UploadLicenseButtons.cshtml @@ -0,0 +1,2 @@ +@model OrderModel.UploadLicenseModel + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/UploadLicenseFilePopup.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/UploadLicenseFilePopup.cshtml deleted file mode 100644 index d8a1b0cc7..000000000 --- a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/UploadLicenseFilePopup.cshtml +++ /dev/null @@ -1,64 +0,0 @@ -@model OrderModel.UploadLicenseModel -@{ - Layout = ""; - //page title - ViewBag.Title = Loc["Admin.Orders.Products.License.UploadFile"]; -} -
- -
-
-
-
-
- - @Loc["Admin.Orders.Products.License.UploadFile"] -
-
-
- - @if (!string.IsNullOrEmpty(Model.LicenseDownloadId)) - { - - } -
- -
-
-
-
- @if (ViewBag.RefreshPage == true) - { - - } - - - - - - \ No newline at end of file diff --git a/src/Web/Grand.Web.Admin/Controllers/OrderController.cs b/src/Web/Grand.Web.Admin/Controllers/OrderController.cs index bf23eb73c..10114abec 100644 --- a/src/Web/Grand.Web.Admin/Controllers/OrderController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/OrderController.cs @@ -1,28 +1,33 @@ -using Grand.Business.Core.Commands.Checkout.Orders; +using Grand.Business.Core.Commands.Checkout.Orders; using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Business.Core.Interfaces.Checkout.Orders; using Grand.Business.Core.Interfaces.Checkout.Shipping; -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.Common.Pdf; using Grand.Business.Core.Interfaces.ExportImport; -using Grand.Domain.Catalog; -using Grand.Domain.Common; using Grand.Domain.Orders; using Grand.Domain.Permissions; using Grand.Infrastructure; -using Grand.Web.AdminShared.Extensions; +using Grand.Mediator; +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 Grand.Mediator; using Microsoft.AspNetCore.Mvc; namespace Grand.Web.Admin.Controllers; -[PermissionAuthorize(PermissionSystemName.Orders)] +// Concrete host subclass of BaseOrderManagementController (ARCH-001 Order consolidation). This class +// supplies Admin's DI wiring plus the attributes that used to arrive transitively via +// BaseAdminController - BaseOrderManagementController can't inherit any single host's base controller +// (it's shared across Admin/Store, each with a different [Area]/[Authorize*] pair), so each subclass +// restates its own host's attribute set explicitly, same pattern as CategoryController/ProductController. +[AuthorizeAdmin] +[AutoValidateAntiforgeryToken] +[Area(Constants.AreaAdmin)] +[AuthorizeMenu] public class OrderController( IOrderViewModelService orderViewModelService, IOrderService orderService, @@ -30,106 +35,21 @@ public class OrderController( ITranslationService translationService, IContextAccessor contextAccessor, IPdfService pdfService, - IGroupService groupService, - IExportManager exportManager, - IMediator mediator) - : BaseAdminController + IMediator mediator, + IAdminDataScope scope, + IExportManager exportManager) + : BaseOrderManagementController(orderViewModelService, orderService, orderStatusService, + translationService, contextAccessor, pdfService, mediator, scope) { - #region Utilities - - protected virtual async Task CheckSalesManager(Order order) - { - return await groupService.IsSalesManager(contextAccessor.WorkContext.CurrentCustomer) - && contextAccessor.WorkContext.CurrentCustomer.SeId != order.SeId; - } - - #endregion - - #region Order list - - public IActionResult Index() - { - return RedirectToAction("List"); - } - - public async Task List(int? orderStatusId = null, - int? paymentStatusId = null, int? shippingStatusId = null, DateTime? startDate = null, string code = null) - { - var model = await orderViewModelService.PrepareOrderListModel(orderStatusId, paymentStatusId, shippingStatusId, startDate, "", code); - return View(model); - } - - public async Task ProductSearchAutoComplete(string term, - [FromServices] IProductService productService) - { - const int searchTermMinimumLength = 3; - if (string.IsNullOrWhiteSpace(term) || term.Length < searchTermMinimumLength) - return Content(""); - - //products - const int productNumber = 15; - var products = (await productService.SearchProducts( - keywords: term, - pageSize: productNumber, - showHidden: true)).products; - - var result = (from p in products - select new - { - label = p.Name, - productid = p.Id - }) - .ToList(); - return Json(result); - } - - [PermissionAuthorizeAction(PermissionActionName.List)] - [HttpPost] - public async Task OrderList(DataSourceRequest command, OrderListModel model) - { - var (orderModels, totalCount) = - await orderViewModelService.PrepareOrderModel(model, command.Page, command.PageSize); - - var gridModel = new DataSourceResult { - Data = orderModels.ToList(), - Total = totalCount - }; - return Json(gridModel); - } - - [PermissionAuthorizeAction(PermissionActionName.Preview)] - [HttpPost] - public async Task GoToOrderId(OrderListModel model) - { - Order order = null; - int.TryParse(model.GoDirectlyToNumber, out var orderNumber); - if (orderNumber > 0) order = await orderService.GetOrderByNumber(orderNumber); - var orders = await orderService.GetOrdersByCode(model.GoDirectlyToNumber); - switch (orders.Count) - { - case > 1: - return RedirectToAction("List", new { Code = model.GoDirectlyToNumber }); - case 1: - order = orders.FirstOrDefault(); - break; - } - - if (order == null || await CheckSalesManager(order)) - return RedirectToAction("List"); - - return RedirectToAction("Edit", "Order", new { id = order.Id }); - } - - #endregion - - #region Export + // Admin-exclusive: Store already holds the Export/Delete PermissionActionName grants (used by + // PdfInvoiceAll/PdfInvoiceSelected/Delete today), so these three stay off both base classes - + // see plan's Global Constraints and spec §3.6. [PermissionAuthorizeAction(PermissionActionName.Export)] [HttpPost] public async Task ExportExcelAll(OrderListModel model) { - //load orders - var orders = await orderViewModelService.PrepareOrders(model); + var orders = await OrderViewModelService.PrepareOrders(model); try { var bytes = await exportManager.Export(orders); @@ -149,145 +69,14 @@ public async Task ExportExcelSelected(string selectedIds) var orders = new List(); if (selectedIds != null) { - var ids = selectedIds - .Split([','], StringSplitOptions.RemoveEmptyEntries) - .Select(x => x) - .ToArray(); - orders.AddRange(await orderService.GetOrdersByIds(ids)); + var ids = selectedIds.Split([','], StringSplitOptions.RemoveEmptyEntries).ToArray(); + orders.AddRange(await OrderService.GetOrdersByIds(ids)); } var bytes = await exportManager.Export(orders); return File(bytes, "text/xls", "orders.xlsx"); } - #endregion - - #region Order details - - #region Payments and other order workflow - - [PermissionAuthorizeAction(PermissionActionName.Cancel)] - [HttpGet] - public async Task CancelOrder(string id) - { - var order = await orderService.GetOrderById(id); - if (order == null || await CheckSalesManager(order)) - //No order found with the specified id - return RedirectToAction("List"); - - try - { - await mediator.Send(new CancelOrderCommand { Order = order, NotifyCustomer = true }); - - Success("Successfully canceled order"); - return RedirectToAction("Edit", "Order", new { id }); - } - catch (Exception exc) - { - //error - Error(exc); - return RedirectToAction("Edit", "Order", new { id }); - } - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - [HttpPost] - public async Task SaveOrderTags(OrderModel orderModel) - { - var order = await orderService.GetOrderById(orderModel.Id); - if (order == null || await CheckSalesManager(order)) - //No order found with the specified id - return RedirectToAction("List"); - - try - { - await orderViewModelService.SaveOrderTags(order, orderModel.OrderTags); - - var model = new OrderModel(); - await orderViewModelService.PrepareOrderDetailsModel(model, order); - return RedirectToAction("Edit", "Order", new { id = order.Id }); - } - catch (Exception exception) - { - //error - Error(exception, false); - return RedirectToAction("Edit", "Order", new { id = order.Id }); - } - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - [HttpPost] - public async Task ChangeOrderStatus(string id, OrderModel model) - { - var order = await orderService.GetOrderById(id); - if (order == null || await CheckSalesManager(order)) - //No order found with the specified id - return RedirectToAction("List"); - - try - { - var status = await orderStatusService.GetByStatusId(model.OrderStatusId); - ArgumentNullException.ThrowIfNull(status); - - order.OrderStatusId = model.OrderStatusId; - await orderService.UpdateOrder(order); - - //add a note - await orderService.InsertOrderNote(new OrderNote { - Note = $"Order status has been edited. New status: {status.Name}", - DisplayToCustomer = false, - OrderId = order.Id - }); - model = new OrderModel(); - await orderViewModelService.PrepareOrderDetailsModel(model, order); - return RedirectToAction("Edit", "Order", new { id }); - } - catch (Exception exc) - { - //error - Error(exc, false); - return RedirectToAction("Edit", "Order", new { id }); - } - } - - #endregion - - #region Edit, delete - - [PermissionAuthorizeAction(PermissionActionName.Preview)] - public async Task Edit(string id) - { - var order = await orderService.GetOrderById(id); - if (order == null || order.Deleted || await CheckSalesManager(order)) - //No order found with the specified id - return RedirectToAction("List"); - - var model = new OrderModel(); - await orderViewModelService.PrepareOrderDetailsModel(model, order); - - return View(model); - } - - [PermissionAuthorizeAction(PermissionActionName.Delete)] - [HttpPost] - public async Task Delete(OrderDeleteModel model) - { - var order = await orderService.GetOrderById(model.Id); - if (order == null || await CheckSalesManager(order)) - //No order found with the specified id - return RedirectToAction("List"); - - if (ModelState.IsValid) - { - await mediator.Send(new DeleteOrderCommand { Order = order }); - - return RedirectToAction("List"); - } - - Error(ModelState); - return RedirectToAction("Edit", "Order", new { model.Id }); - } - [PermissionAuthorizeAction(PermissionActionName.Delete)] [HttpPost] public async Task DeleteSelected( @@ -297,579 +86,17 @@ public async Task DeleteSelected( if (selectedIds != null) { var orders = new List(); - orders.AddRange(await orderService.GetOrdersByIds(selectedIds.ToArray())); - for (var i = 0; i < orders.Count; i++) + orders.AddRange(await OrderService.GetOrdersByIds(selectedIds.ToArray())); + foreach (var order in orders) { - var order = orders[i]; var shipments = await shipmentService.GetShipmentsByOrder(order.Id); if (shipments.Any()) Error("Some orders is in associated with shipments. Please delete it first."); - - if (!shipments.Any()) await mediator.Send(new DeleteOrderCommand { Order = order }); + else + await Mediator.Send(new DeleteOrderCommand { Order = order }); } } return Json(new { Result = true }); } - - public async Task PdfInvoice(string orderId) - { - var order = await orderService.GetOrderById(orderId); - if (await CheckSalesManager(order)) return RedirectToAction("List"); - - var orders = new List { - order - }; - byte[] bytes; - using (var stream = new MemoryStream()) - { - await pdfService.PrintOrdersToPdf(stream, orders, contextAccessor.WorkContext.WorkingLanguage.Id); - bytes = stream.ToArray(); - } - - return File(bytes, "application/pdf", $"order_{order.Id}.pdf"); - } - - [PermissionAuthorizeAction(PermissionActionName.Export)] - [HttpPost] - public async Task PdfInvoiceAll(OrderListModel model) - { - //load orders - var orders = await orderViewModelService.PrepareOrders(model); - - byte[] bytes; - using (var stream = new MemoryStream()) - { - await pdfService.PrintOrdersToPdf(stream, orders, contextAccessor.WorkContext.WorkingLanguage.Id, model.VendorId); - bytes = stream.ToArray(); - } - - return File(bytes, "application/pdf", "orders.pdf"); - } - - [PermissionAuthorizeAction(PermissionActionName.Export)] - [HttpPost] - public async Task PdfInvoiceSelected(string selectedIds) - { - var orders = new List(); - if (selectedIds != null) - { - var ids = selectedIds - .Split([','], StringSplitOptions.RemoveEmptyEntries) - .Select(x => x) - .ToArray(); - orders.AddRange(await orderService.GetOrdersByIds(ids)); - } - - //ensure that we at least one order selected - if (orders.Count == 0) - { - Error(translationService.GetResource("Admin.Orders.PdfInvoice.NoOrders")); - return RedirectToAction("List"); - } - - byte[] bytes; - using (var stream = new MemoryStream()) - { - await pdfService.PrintOrdersToPdf(stream, orders, contextAccessor.WorkContext.WorkingLanguage.Id); - bytes = stream.ToArray(); - } - - return File(bytes, "application/pdf", "orders.pdf"); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - [HttpPost] - public async Task EditOrderTotals(string id, OrderModel model) - { - var order = await orderService.GetOrderById(id); - if (order == null || await CheckSalesManager(order)) - //No order found with the specified id - return RedirectToAction("List"); - - order.OrderSubtotalInclTax = model.OrderSubtotalInclTaxValue; - order.OrderSubtotalExclTax = model.OrderSubtotalExclTaxValue; - order.OrderSubTotalDiscountInclTax = model.OrderSubTotalDiscountInclTaxValue; - order.OrderSubTotalDiscountExclTax = model.OrderSubTotalDiscountExclTaxValue; - order.OrderShippingInclTax = model.OrderShippingInclTaxValue; - order.OrderShippingExclTax = model.OrderShippingExclTaxValue; - order.PaymentMethodAdditionalFeeInclTax = model.PaymentMethodAdditionalFeeInclTaxValue; - order.PaymentMethodAdditionalFeeExclTax = model.PaymentMethodAdditionalFeeExclTaxValue; - order.OrderTax = model.TaxValue; - order.OrderDiscount = model.OrderTotalDiscountValue; - order.OrderTotal = model.OrderTotalValue; - order.CurrencyRate = model.CurrencyRate; - await orderService.UpdateOrder(order); - - //add a note - await orderService.InsertOrderNote(new OrderNote { - Note = "Order totals have been edited", - DisplayToCustomer = false, - OrderId = order.Id - }); - - await orderViewModelService.PrepareOrderDetailsModel(model, order); - return RedirectToAction("Edit", "Order", new { id }); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - [HttpPost] - public async Task EditShippingMethod(string id, OrderModel model) - { - var order = await orderService.GetOrderById(id); - if (order == null || await CheckSalesManager(order)) - //No order found with the specified id - return RedirectToAction("List"); - - order.ShippingMethod = model.ShippingMethod; - await orderService.UpdateOrder(order); - - //add a note - await orderService.InsertOrderNote(new OrderNote { - Note = "Shipping method has been edited", - DisplayToCustomer = false, - OrderId = order.Id - }); - await orderViewModelService.PrepareOrderDetailsModel(model, order); - - //selected tab - await SaveSelectedTabIndex(persistForTheNextRequest: true); - - return RedirectToAction("Edit", "Order", new { id }); - } - - [HttpPost] - public async Task EditUserFields(string id, OrderModel model) - { - var order = await orderService.GetOrderById(id); - if (order == null || await CheckSalesManager(order)) - //No order found with the specified id - return RedirectToAction("List"); - - order.UserFields = model.UserFields; - - await orderService.UpdateOrder(order); - - await orderViewModelService.PrepareOrderDetailsModel(model, order); - - //selected tab - await SaveSelectedTabIndex(persistForTheNextRequest: true); - - return RedirectToAction("Edit", "Order", new { id }); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - [HttpPost] - public async Task SaveOrderItem(string id, OrderItemsModel model) - { - var order = await orderService.GetOrderById(id); - if (order == null || await CheckSalesManager(order)) - //No order found with the specified id - return RedirectToAction("List"); - - if (order.OrderStatusId == (int)OrderStatusSystem.Cancelled) - { - Error("You can't edit position when order is canceled"); - return RedirectToAction("Edit", "Order", new { id }); - } - - var orderItem = order.OrderItems.FirstOrDefault(x => x.Id == model.OrderItemId) ?? throw new ArgumentException("No order item found with the specified id"); - var itemModel = model.Items.FirstOrDefault(x => x.Id == model.OrderItemId) ?? throw new ArgumentException("No order item model found with the specified id"); - - if (itemModel.Quantity == 0 || (orderItem.OpenQty != orderItem.Quantity && orderItem.IsShipEnabled)) - { - Error("You can't change quantity"); - return RedirectToAction("Edit", "Order", new { id }); - } - - if (orderItem.Quantity == itemModel.Quantity && orderItem.UnitPriceExclTax == itemModel.UnitPriceExclTaxValue) - { - Error("Nothing has been changed"); - return RedirectToAction("Edit", "Order", new { id }); - } - - orderItem.Quantity = itemModel.Quantity; - orderItem.OpenQty = itemModel.Quantity; - - if (orderItem.UnitPriceExclTax != itemModel.UnitPriceExclTaxValue) - { - orderItem.UnitPriceExclTax = itemModel.UnitPriceExclTaxValue; - orderItem.UnitPriceInclTax = - Math.Round(orderItem.UnitPriceExclTax * orderItem.TaxRate / 100 + orderItem.UnitPriceExclTax, 2); - orderItem.PriceInclTax = Math.Round(orderItem.UnitPriceInclTax * orderItem.Quantity, 2); - orderItem.PriceExclTax = Math.Round(orderItem.UnitPriceExclTax * orderItem.Quantity, 2); - - orderItem.DiscountAmountInclTax = 0; - orderItem.DiscountAmountExclTax = 0; - } - else - { - orderItem.PriceInclTax = Math.Round(orderItem.UnitPriceInclTax * orderItem.Quantity, 2); - orderItem.PriceExclTax = Math.Round(orderItem.UnitPriceExclTax * orderItem.Quantity, 2); - - orderItem.DiscountAmountInclTax = 0; - orderItem.DiscountAmountExclTax = 0; - } - - await mediator.Send(new UpdateOrderItemCommand { Order = order, OrderItem = orderItem }); - - //selected tab - await SaveSelectedTabIndex(persistForTheNextRequest: true); - - return RedirectToAction("Edit", "Order", new { id }); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - [HttpPost] - public async Task DeleteOrderItem(string id, string orderItemId) - { - var order = await orderService.GetOrderById(id); - if (order == null || await CheckSalesManager(order)) - //No order found with the specified id - return RedirectToAction("List"); - - var orderItem = order.OrderItems.FirstOrDefault(x => x.Id == orderItemId) ?? throw new ArgumentException("No order item found with the specified id"); - var result = await mediator.Send(new DeleteOrderItemCommand { Order = order, OrderItem = orderItem }); - if (result.error) - Error(result.message); - - //selected tab - await SaveSelectedTabIndex(persistForTheNextRequest: true); - - return RedirectToAction("Edit", "Order", new { id }); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - [HttpPost] - public async Task CancelOrderItem(string id, string orderItemId) - { - var order = await orderService.GetOrderById(id); - if (order == null || await CheckSalesManager(order)) - //No order found with the specified id - return RedirectToAction("List"); - - var orderItem = order.OrderItems.FirstOrDefault(x => x.Id == orderItemId) ?? throw new ArgumentException("No order item found with the specified id"); - var result = await mediator.Send(new CancelOrderItemCommand { Order = order, OrderItem = orderItem }); - if (result.error) - Error(result.message); - else - Success("The order item was successfully canceled"); - - //selected tab - await SaveSelectedTabIndex(persistForTheNextRequest: true); - - return RedirectToAction("Edit", "Order", new { id }); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - [HttpPost] - public async Task ResetDownloadCount(string id, string orderItemId) - { - var order = await orderService.GetOrderById(id); - if (order == null || await CheckSalesManager(order)) - //No order found with the specified id - return RedirectToAction("List"); - - var orderItem = order.OrderItems.FirstOrDefault(x => x.Id == orderItemId) ?? throw new ArgumentException("No order item found with the specified id"); - orderItem.DownloadCount = 0; - await orderService.UpdateOrder(order); - var model = new OrderModel(); - await orderViewModelService.PrepareOrderDetailsModel(model, order); - - //selected tab - await SaveSelectedTabIndex(persistForTheNextRequest: true); - - return RedirectToAction("Edit", "Order", new { id }); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - [HttpPost] - public async Task ActivateDownloadItem(string id, string orderItemId) - { - var order = await orderService.GetOrderById(id); - if (order == null || await CheckSalesManager(order)) - //No order found with the specified id - return RedirectToAction("List"); - - var orderItem = order.OrderItems.FirstOrDefault(x => x.Id == orderItemId) ?? throw new ArgumentException("No order item found with the specified id"); - orderItem.IsDownloadActivated = !orderItem.IsDownloadActivated; - await orderService.UpdateOrder(order); - var model = new OrderModel(); - await orderViewModelService.PrepareOrderDetailsModel(model, order); - - //selected tab - await SaveSelectedTabIndex(persistForTheNextRequest: true); - - return RedirectToAction("Edit", "Order", new { id }); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - public async Task UploadLicenseFilePopup(string id, string orderItemId, - [FromServices] IProductService productService) - { - var order = await orderService.GetOrderById(id); - if (order == null || await CheckSalesManager(order)) - //No order found with the specified id - return RedirectToAction("List"); - - var orderItem = order.OrderItems.FirstOrDefault(x => x.Id == orderItemId) ?? throw new ArgumentException("No order item found with the specified id"); - var product = await productService.GetProductByIdIncludeArch(orderItem.ProductId); - - if (!product.IsDownload) - throw new ArgumentException("Product is not downloadable"); - var model = new OrderModel.UploadLicenseModel { - LicenseDownloadId = !string.IsNullOrEmpty(orderItem.LicenseDownloadId) ? orderItem.LicenseDownloadId : "", - OrderId = order.Id, - OrderItemId = orderItem.Id - }; - - return View(model); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - [HttpPost] - public async Task UploadLicenseFilePopup(OrderModel.UploadLicenseModel model) - { - var order = await orderService.GetOrderById(model.OrderId); - if (order == null || await CheckSalesManager(order)) - //No order found with the specified id - return RedirectToAction("List"); - - var orderItem = order.OrderItems.FirstOrDefault(x => x.Id == model.OrderItemId) ?? throw new ArgumentException("No order item found with the specified id"); - - //attach license - orderItem.LicenseDownloadId = !string.IsNullOrEmpty(model.LicenseDownloadId) ? model.LicenseDownloadId : null; - await orderService.UpdateOrder(order); - - //success - ViewBag.RefreshPage = true; - - return View(model); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - [HttpPost] - public async Task DeleteLicenseFilePopup(OrderModel.UploadLicenseModel model) - { - var order = await orderService.GetOrderById(model.OrderId); - if (order == null || await CheckSalesManager(order)) - //No order found with the specified id - return RedirectToAction("List"); - - var orderItem = order.OrderItems.FirstOrDefault(x => x.Id == model.OrderItemId) ?? throw new ArgumentException("No order item found with the specified id"); - - //attach license - orderItem.LicenseDownloadId = null; - await orderService.UpdateOrder(order); - - //success - ViewBag.RefreshPage = true; - - return RedirectToAction("Edit", "Order", new { id = model.OrderId }); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - public async Task AddProductToOrder(string orderId) - { - var order = await orderService.GetOrderById(orderId); - if (order == null || await CheckSalesManager(order)) - //No order found with the specified id - return RedirectToAction("List"); - - var model = await orderViewModelService.PrepareAddOrderProductModel(order); - return View(model); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - [HttpPost] - public async Task AddProductToOrder(DataSourceRequest command, OrderModel.AddOrderProductModel model, - [FromServices] IProductService productService) - { - var categoryIds = new List(); - if (!string.IsNullOrEmpty(model.SearchCategoryId)) - categoryIds.Add(model.SearchCategoryId); - - var gridModel = new DataSourceResult(); - var products = (await productService.SearchProducts(categoryIds: categoryIds, - brandId: model.SearchBrandId, - collectionId: model.SearchCollectionId, - productType: model.SearchProductTypeId > 0 ? (ProductType?)model.SearchProductTypeId : null, - keywords: model.SearchProductName, - pageIndex: command.Page - 1, - pageSize: command.PageSize, - showHidden: true)).products; - gridModel.Data = products.Select(x => - { - var productModel = new OrderModel.AddOrderProductModel.ProductModel { - Id = x.Id, - Name = x.Name, - Sku = x.Sku - }; - - return productModel; - }); - gridModel.Total = products.TotalCount; - - return Json(gridModel); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - public async Task AddProductToOrderDetails(string orderId, string productId) - { - var order = await orderService.GetOrderById(orderId); - if (order == null || await CheckSalesManager(order)) - return RedirectToAction("List"); - - var model = await orderViewModelService.PrepareAddProductToOrderModel(order, productId); - return View(model); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - [HttpPost] - public async Task AddProductToOrderDetails(AddProductToOrderModel model) - { - var order = await orderService.GetOrderById(model.OrderId); - if (order == null || await CheckSalesManager(order)) - return RedirectToAction("List"); - - var warnings = await orderViewModelService.AddProductToOrderDetails(model); - if (!warnings.Any()) - //redirect to order details page - return RedirectToAction("Edit", "Order", new { id = model.OrderId }); - - //errors - var result = await orderViewModelService.PrepareAddProductToOrderModel(order, model.ProductId); - result.Warnings.AddRange(warnings); - return View(result); - } - - #endregion - - #endregion - - #region Addresses - - [PermissionAuthorizeAction(PermissionActionName.Preview)] - public async Task AddressEdit(string addressId, string orderId, bool billingAddress) - { - var order = await orderService.GetOrderById(orderId); - if (order == null || await CheckSalesManager(order)) - //No order found with the specified id - return RedirectToAction("List"); - - var address = new Address(); - switch (billingAddress) - { - case true when order.BillingAddress != null: - { - if (order.BillingAddress.Id == addressId) - address = order.BillingAddress; - break; - } - case false when order.ShippingAddress != null: - { - if (order.ShippingAddress.Id == addressId) - address = order.ShippingAddress; - break; - } - } - - if (address == null) - throw new ArgumentException("No address found with the specified id", nameof(addressId)); - - var model = await orderViewModelService.PrepareOrderAddressModel(order, address); - model.BillingAddress = billingAddress; - return View(model); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - [HttpPost] - public async Task AddressEdit(OrderAddressModel model, - [FromServices] IAddressAttributeService addressAttributeService, - [FromServices] IAddressAttributeParser addressAttributeParser) - { - var order = await orderService.GetOrderById(model.OrderId); - if (order == null || await CheckSalesManager(order)) - //No order found with the specified id - return RedirectToAction("List"); - - var address = new Address(); - switch (model.BillingAddress) - { - case true when order.BillingAddress != null: - { - if (order.BillingAddress.Id == model.Address.Id) - address = order.BillingAddress; - break; - } - case false when order.ShippingAddress != null: - { - if (order.ShippingAddress.Id == model.Address.Id) - address = order.ShippingAddress; - break; - } - } - - if (ModelState.IsValid) - { - var customAttributes = - await model.Address.ParseCustomAddressAttributes(addressAttributeParser, addressAttributeService); - await orderViewModelService.UpdateOrderAddress(order, address, model, customAttributes); - return RedirectToAction("AddressEdit", - new { addressId = model.Address.Id, orderId = model.OrderId, model.BillingAddress }); - } - - //If we got this far, something failed, redisplay form - model = await orderViewModelService.PrepareOrderAddressModel(order, address); - return View(model); - } - - #endregion - - #region Order notes - - [PermissionAuthorizeAction(PermissionActionName.List)] - [HttpPost] - public async Task OrderNotesSelect(string orderId, DataSourceRequest command) - { - var order = await orderService.GetOrderById(orderId); - if (order == null || await CheckSalesManager(order)) - throw new ArgumentException("No order found with the specified id"); - - //order notes - var orderNoteModels = await orderViewModelService.PrepareOrderNotes(order); - var gridModel = new DataSourceResult { - Data = orderNoteModels, - Total = orderNoteModels.Count - }; - return Json(gridModel); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - public async Task OrderNoteAdd(string orderId, string downloadId, bool displayToCustomer, - string message) - { - var order = await orderService.GetOrderById(orderId); - if (order == null || await CheckSalesManager(order)) - return Json(new { Result = false }); - - await orderViewModelService.InsertOrderNote(order, downloadId, displayToCustomer, message); - - return Json(new { Result = true }); - } - - [PermissionAuthorizeAction(PermissionActionName.Delete)] - [HttpPost] - public async Task OrderNoteDelete(string id, string orderId) - { - var order = await orderService.GetOrderById(orderId); - if (order == null || await CheckSalesManager(order)) - throw new ArgumentException("No order found with the specified id"); - - await orderViewModelService.DeleteOrderNote(order, id); - - return new JsonResult(""); - } - - #endregion -} \ No newline at end of file +} diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseOrderController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseOrderController.cs new file mode 100644 index 000000000..d198b2a63 --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseOrderController.cs @@ -0,0 +1,218 @@ +using Grand.Business.Core.Interfaces.Checkout.Orders; +using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Interfaces.Common.Pdf; +using Grand.Domain.Orders; +using Grand.Domain.Permissions; +using Grand.Infrastructure; +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.Orders)] +[AutoValidateAntiforgeryToken] +public abstract class BaseOrderController( + IOrderViewModelService orderViewModelService, + IOrderService orderService, + ITranslationService translationService, + IContextAccessor contextAccessor, + IPdfService pdfService, + IAdminDataScope scope) + : BaseController +{ + // Exposed for BaseOrderManagementController (primary-constructor parameters aren't visible to + // derived classes by name in C#). + protected IOrderViewModelService OrderViewModelService => orderViewModelService; + protected IOrderService OrderService => orderService; + protected ITranslationService TranslationService => translationService; + protected IContextAccessor ContextAccessor => contextAccessor; + protected IPdfService PdfService => pdfService; + protected IAdminDataScope Scope => scope; + + /// DRY replacement for the ~20x-duplicated + /// "load order, redirect to List if not found or not authorized" pattern found identically in + /// both Admin's and Store's original controllers (every action in both files redirects to + /// "List", never "Edit", on either condition). Not a behavior change — every call site below + /// still individually returns RedirectToAction("List") exactly as both originals did. + protected async Task<(Order order, IActionResult denied)> LoadAuthorizedOrder(string id) + { + var order = await orderService.GetOrderById(id); + if (order == null) return (null, RedirectToAction("List")); + if (!await scope.HasAccess(order)) return (null, RedirectToAction("List")); + return (order, null); + } + + #region Order list + + public IActionResult Index() => RedirectToAction("List"); + + public async Task List(int? orderStatusId = null, int? paymentStatusId = null, + int? shippingStatusId = null, DateTime? startDate = null, string code = null) + { + var model = await orderViewModelService.PrepareOrderListModel(orderStatusId, paymentStatusId, + shippingStatusId, startDate, scope.DefaultStoreId ?? "", code); + return View(model); + } + + [PermissionAuthorizeAction(PermissionActionName.List)] + [HttpPost] + public async Task OrderList(DataSourceRequest command, OrderListModel model) + { + if (scope.DefaultStoreId is not null) model.StoreId = scope.DefaultStoreId; + if (scope.DefaultVendorId is not null) model.VendorId = scope.DefaultVendorId; + + var (orderModels, totalCount) = + await orderViewModelService.PrepareOrderModel(model, command.Page, command.PageSize); + + var gridModel = new DataSourceResult { + Data = orderModels.ToList(), + Total = totalCount + }; + return Json(gridModel); + } + + #endregion + + #region Order details (view-only) + + [PermissionAuthorizeAction(PermissionActionName.Preview)] + public async Task Edit(string id) + { + var order = await orderService.GetOrderById(id); + if (order == null || order.Deleted) return RedirectToAction("List"); + if (!await scope.HasAccess(order)) return RedirectToAction("List"); + + var model = new OrderModel(); + await orderViewModelService.PrepareOrderDetailsModel(model, order); + + return View(model); + } + + public async Task PdfInvoice(string orderId) + { + var (order, denied) = await LoadAuthorizedOrder(orderId); + if (denied != null) return denied; + + var orders = new List { order }; + byte[] bytes; + using (var stream = new MemoryStream()) + { + await pdfService.PrintOrdersToPdf(stream, orders, contextAccessor.WorkContext.WorkingLanguage.Id, + scope.DefaultVendorId); + bytes = stream.ToArray(); + } + + return File(bytes, "application/pdf", $"order_{order.Id}.pdf"); + } + + [PermissionAuthorizeAction(PermissionActionName.Export)] + [HttpPost] + public async Task PdfInvoiceAll(OrderListModel model) + { + var orders = await orderViewModelService.PrepareOrders(model); + // Store's original code post-filtered by StoreId here even though PrepareOrders already + // takes model.StoreId as a search filter - keep the extra filter for defense in depth, + // matching Store's original exactly; harmless no-op for Admin/Vendor (DefaultStoreId null). + if (scope.DefaultStoreId is not null) + orders = orders.Where(x => x.StoreId == scope.DefaultStoreId).ToList(); + + byte[] bytes; + using (var stream = new MemoryStream()) + { + await pdfService.PrintOrdersToPdf(stream, orders, contextAccessor.WorkContext.WorkingLanguage.Id, + scope.DefaultVendorId ?? model.VendorId); + bytes = stream.ToArray(); + } + + return File(bytes, "application/pdf", "orders.pdf"); + } + + [PermissionAuthorizeAction(PermissionActionName.Export)] + [HttpPost] + public async Task PdfInvoiceSelected(string selectedIds) + { + var orders = new List(); + if (selectedIds != null) + { + var ids = selectedIds.Split([','], StringSplitOptions.RemoveEmptyEntries).ToArray(); + orders.AddRange(await orderService.GetOrdersByIds(ids)); + } + + // Store filters by StoreId; Vendor's original filtered by HasAccessToOrder (any-item + // vendor match); Admin's original has no filter here at all. scope.HasAccess already + // expresses all three checks per-host, so applying it unconditionally is a deliberate, + // disclosed, security-positive behavior change for Admin: it closes a pre-existing gap + // where a Sales Manager could previously export any order id via a crafted selectedIds + // list, bypassing the Sales-Manager scoping that AdminOrderDataScope.HasAccess enforces + // everywhere else in this controller. + var accessible = new List(); + foreach (var order in orders) + if (await scope.HasAccess(order)) + accessible.Add(order); + orders = accessible; + + if (orders.Count == 0) + { + Error(translationService.GetResource($"{scope.ResourceKeyPrefix}.Orders.PdfInvoice.NoOrders")); + return RedirectToAction("List"); + } + + byte[] bytes; + using (var stream = new MemoryStream()) + { + await pdfService.PrintOrdersToPdf(stream, orders, contextAccessor.WorkContext.WorkingLanguage.Id, + scope.DefaultVendorId); + bytes = stream.ToArray(); + } + + return File(bytes, "application/pdf", "orders.pdf"); + } + + public async Task ProductSearchAutoComplete(string term, + [FromServices] Grand.Business.Core.Interfaces.Catalog.Products.IProductService productService) + { + const int searchTermMinimumLength = 3; + if (string.IsNullOrWhiteSpace(term) || term.Length < searchTermMinimumLength) + return Content(""); + + const int productNumber = 15; + var products = (await productService.SearchProducts( + storeId: scope.DefaultStoreId, + vendorId: scope.DefaultVendorId, + keywords: term, + pageSize: productNumber, + showHidden: true)).products; + + var result = products.Select(p => new { label = p.Name, productid = p.Id }).ToList(); + return Json(result); + } + + [PermissionAuthorizeAction(PermissionActionName.Preview)] + [HttpPost] + public async Task GoToOrderId(OrderListModel model) + { + Order order = null; + int.TryParse(model.GoDirectlyToNumber, out var orderNumber); + if (orderNumber > 0) order = await orderService.GetOrderByNumber(orderNumber); + else + { + var orders = await orderService.GetOrdersByCode(model.GoDirectlyToNumber); + switch (orders.Count) + { + case > 1: return RedirectToAction("List", new { Code = model.GoDirectlyToNumber }); + case 1: order = orders.FirstOrDefault(); break; + case 0: return RedirectToAction("List", new { Code = model.GoDirectlyToNumber }); + } + } + + if (order == null || !await scope.HasAccess(order)) return RedirectToAction("List"); + + return RedirectToAction("Edit", "Order", new { id = order.Id }); + } + + #endregion +} diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseOrderManagementController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseOrderManagementController.cs new file mode 100644 index 000000000..82315436c --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseOrderManagementController.cs @@ -0,0 +1,564 @@ +using Grand.Business.Core.Commands.Checkout.Orders; +using Grand.Business.Core.Interfaces.Checkout.Orders; +using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Interfaces.Common.Pdf; +using Grand.Domain.Orders; +using Grand.Domain.Permissions; +using Grand.Infrastructure; +using Grand.Mediator; +using Grand.Web.AdminShared.Extensions; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Orders; +using Grand.Web.Common.Security.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Grand.Web.AdminShared.Controllers; + +/// +/// Every mutating Order action. Base for Admin and Store only — Vendor's concrete controller +/// inherits directly, so none of these actions exist on its +/// type at all (not permission-gated, genuinely absent — see ARCH-001 Order consolidation spec +/// §3.5). +/// +public abstract class BaseOrderManagementController( + IOrderViewModelService orderViewModelService, + IOrderService orderService, + IOrderStatusService orderStatusService, + ITranslationService translationService, + IContextAccessor contextAccessor, + IPdfService pdfService, + IMediator mediator, + IAdminDataScope scope) + : BaseOrderController(orderViewModelService, orderService, translationService, contextAccessor, + pdfService, scope) +{ + // Exposed for Grand.Web.Admin's concrete OrderController subclass (Task 17), which calls + // Mediator.Send(...) directly - primary-constructor parameters aren't visible to derived + // classes by name in C#. + protected IMediator Mediator => mediator; + + #region Payments and other order workflow + + [PermissionAuthorizeAction(PermissionActionName.Cancel)] + [HttpGet] + public async Task CancelOrder(string id) + { + var (order, denied) = await LoadAuthorizedOrder(id); + if (denied != null) return denied; + + try + { + await mediator.Send(new CancelOrderCommand { Order = order, NotifyCustomer = true }); + Success("Successfully canceled order"); + return RedirectToAction("Edit", "Order", new { id }); + } + catch (Exception exc) + { + Error(exc); + return RedirectToAction("Edit", "Order", new { id }); + } + } + + [PermissionAuthorizeAction(PermissionActionName.Edit)] + [HttpPost] + public async Task SaveOrderTags(OrderModel orderModel) + { + var (order, denied) = await LoadAuthorizedOrder(orderModel.Id); + if (denied != null) return denied; + + try + { + await orderViewModelService.SaveOrderTags(order, orderModel.OrderTags); + var model = new OrderModel(); + await orderViewModelService.PrepareOrderDetailsModel(model, order); + return RedirectToAction("Edit", "Order", new { id = order.Id }); + } + catch (Exception exception) + { + Error(exception, false); + return RedirectToAction("Edit", "Order", new { id = order.Id }); + } + } + + [PermissionAuthorizeAction(PermissionActionName.Edit)] + [HttpPost] + public async Task ChangeOrderStatus(string id, OrderModel model) + { + var (order, denied) = await LoadAuthorizedOrder(id); + if (denied != null) return denied; + + try + { + var status = await orderStatusService.GetByStatusId(model.OrderStatusId); + ArgumentNullException.ThrowIfNull(status); + + order.OrderStatusId = model.OrderStatusId; + await orderService.UpdateOrder(order); + + await orderService.InsertOrderNote(new OrderNote { + Note = $"Order status has been edited. New status: {status.Name}", + DisplayToCustomer = false, + OrderId = order.Id + }); + model = new OrderModel(); + await orderViewModelService.PrepareOrderDetailsModel(model, order); + return RedirectToAction("Edit", "Order", new { id }); + } + catch (Exception exc) + { + Error(exc, false); + return RedirectToAction("Edit", "Order", new { id }); + } + } + + #endregion + + #region Order totals / shipping / user fields + + [PermissionAuthorizeAction(PermissionActionName.Edit)] + [HttpPost] + public async Task EditOrderTotals(string id, OrderModel model) + { + var (order, denied) = await LoadAuthorizedOrder(id); + if (denied != null) return denied; + + order.OrderSubtotalInclTax = model.OrderSubtotalInclTaxValue; + order.OrderSubtotalExclTax = model.OrderSubtotalExclTaxValue; + order.OrderSubTotalDiscountInclTax = model.OrderSubTotalDiscountInclTaxValue; + order.OrderSubTotalDiscountExclTax = model.OrderSubTotalDiscountExclTaxValue; + order.OrderShippingInclTax = model.OrderShippingInclTaxValue; + order.OrderShippingExclTax = model.OrderShippingExclTaxValue; + order.PaymentMethodAdditionalFeeInclTax = model.PaymentMethodAdditionalFeeInclTaxValue; + order.PaymentMethodAdditionalFeeExclTax = model.PaymentMethodAdditionalFeeExclTaxValue; + order.OrderTax = model.TaxValue; + order.OrderDiscount = model.OrderTotalDiscountValue; + order.OrderTotal = model.OrderTotalValue; + order.CurrencyRate = model.CurrencyRate; + await orderService.UpdateOrder(order); + + await orderService.InsertOrderNote(new OrderNote { + Note = "Order totals have been edited", + DisplayToCustomer = false, + OrderId = order.Id + }); + + await orderViewModelService.PrepareOrderDetailsModel(model, order); + return RedirectToAction("Edit", "Order", new { id }); + } + + [PermissionAuthorizeAction(PermissionActionName.Edit)] + [HttpPost] + public async Task EditShippingMethod(string id, OrderModel model) + { + var (order, denied) = await LoadAuthorizedOrder(id); + if (denied != null) return denied; + + order.ShippingMethod = model.ShippingMethod; + await orderService.UpdateOrder(order); + + await orderService.InsertOrderNote(new OrderNote { + Note = "Shipping method has been edited", + DisplayToCustomer = false, + OrderId = order.Id + }); + await orderViewModelService.PrepareOrderDetailsModel(model, order); + + await SaveSelectedTabIndex(persistForTheNextRequest: true); + return RedirectToAction("Edit", "Order", new { id }); + } + + [HttpPost] + public async Task EditUserFields(string id, OrderModel model) + { + var (order, denied) = await LoadAuthorizedOrder(id); + if (denied != null) return denied; + + order.UserFields = model.UserFields; + await orderService.UpdateOrder(order); + await orderViewModelService.PrepareOrderDetailsModel(model, order); + + await SaveSelectedTabIndex(persistForTheNextRequest: true); + return RedirectToAction("Edit", "Order", new { id }); + } + + #endregion + + #region Edit, delete + + [PermissionAuthorizeAction(PermissionActionName.Delete)] + [HttpPost] + public async Task Delete(OrderDeleteModel model) + { + var (order, denied) = await LoadAuthorizedOrder(model.Id); + if (denied != null) return denied; + + if (ModelState.IsValid) + { + await mediator.Send(new DeleteOrderCommand { Order = order }); + return RedirectToAction("List"); + } + + Error(ModelState); + return RedirectToAction("Edit", "Order", new { model.Id }); + } + + #endregion + + #region Order items + + [PermissionAuthorizeAction(PermissionActionName.Edit)] + [HttpPost] + public async Task SaveOrderItem(string id, OrderItemsModel model) + { + var (order, denied) = await LoadAuthorizedOrder(id); + if (denied != null) return denied; + + if (order.OrderStatusId == (int)OrderStatusSystem.Cancelled) + { + Error("You can't edit position when order is canceled"); + return RedirectToAction("Edit", "Order", new { id }); + } + + var orderItem = order.OrderItems.FirstOrDefault(x => x.Id == model.OrderItemId) + ?? throw new ArgumentException("No order item found with the specified id"); + var itemModel = model.Items.FirstOrDefault(x => x.Id == model.OrderItemId) + ?? throw new ArgumentException("No order item model found with the specified id"); + + if (itemModel.Quantity == 0 || (orderItem.OpenQty != orderItem.Quantity && orderItem.IsShipEnabled)) + { + Error("You can't change quantity"); + return RedirectToAction("Edit", "Order", new { id }); + } + + if (orderItem.Quantity == itemModel.Quantity && orderItem.UnitPriceExclTax == itemModel.UnitPriceExclTaxValue) + { + Error("Nothing has been changed"); + return RedirectToAction("Edit", "Order", new { id }); + } + + orderItem.Quantity = itemModel.Quantity; + orderItem.OpenQty = itemModel.Quantity; + + if (orderItem.UnitPriceExclTax != itemModel.UnitPriceExclTaxValue) + { + orderItem.UnitPriceExclTax = itemModel.UnitPriceExclTaxValue; + orderItem.UnitPriceInclTax = + Math.Round(orderItem.UnitPriceExclTax * orderItem.TaxRate / 100 + orderItem.UnitPriceExclTax, 2); + orderItem.PriceInclTax = Math.Round(orderItem.UnitPriceInclTax * orderItem.Quantity, 2); + orderItem.PriceExclTax = Math.Round(orderItem.UnitPriceExclTax * orderItem.Quantity, 2); + orderItem.DiscountAmountInclTax = 0; + orderItem.DiscountAmountExclTax = 0; + } + else + { + orderItem.PriceInclTax = Math.Round(orderItem.UnitPriceInclTax * orderItem.Quantity, 2); + orderItem.PriceExclTax = Math.Round(orderItem.UnitPriceExclTax * orderItem.Quantity, 2); + orderItem.DiscountAmountInclTax = 0; + orderItem.DiscountAmountExclTax = 0; + } + + await mediator.Send(new UpdateOrderItemCommand { Order = order, OrderItem = orderItem }); + await SaveSelectedTabIndex(persistForTheNextRequest: true); + return RedirectToAction("Edit", "Order", new { id }); + } + + [PermissionAuthorizeAction(PermissionActionName.Edit)] + [HttpPost] + public async Task DeleteOrderItem(string id, string orderItemId) + { + var (order, denied) = await LoadAuthorizedOrder(id); + if (denied != null) return denied; + + var orderItem = order.OrderItems.FirstOrDefault(x => x.Id == orderItemId) + ?? throw new ArgumentException("No order item found with the specified id"); + var result = await mediator.Send(new DeleteOrderItemCommand { Order = order, OrderItem = orderItem }); + if (result.error) Error(result.message); + + await SaveSelectedTabIndex(persistForTheNextRequest: true); + return RedirectToAction("Edit", "Order", new { id }); + } + + [PermissionAuthorizeAction(PermissionActionName.Edit)] + [HttpPost] + public async Task CancelOrderItem(string id, string orderItemId) + { + var (order, denied) = await LoadAuthorizedOrder(id); + if (denied != null) return denied; + + var orderItem = order.OrderItems.FirstOrDefault(x => x.Id == orderItemId) + ?? throw new ArgumentException("No order item found with the specified id"); + var result = await mediator.Send(new CancelOrderItemCommand { Order = order, OrderItem = orderItem }); + if (result.error) Error(result.message); + else Success("The order item was successfully canceled"); + + await SaveSelectedTabIndex(persistForTheNextRequest: true); + return RedirectToAction("Edit", "Order", new { id }); + } + + [PermissionAuthorizeAction(PermissionActionName.Edit)] + [HttpPost] + public async Task ResetDownloadCount(string id, string orderItemId) + { + var (order, denied) = await LoadAuthorizedOrder(id); + if (denied != null) return denied; + + var orderItem = order.OrderItems.FirstOrDefault(x => x.Id == orderItemId) + ?? throw new ArgumentException("No order item found with the specified id"); + orderItem.DownloadCount = 0; + await orderService.UpdateOrder(order); + var model = new OrderModel(); + await orderViewModelService.PrepareOrderDetailsModel(model, order); + + await SaveSelectedTabIndex(persistForTheNextRequest: true); + return RedirectToAction("Edit", "Order", new { id }); + } + + [PermissionAuthorizeAction(PermissionActionName.Edit)] + [HttpPost] + public async Task ActivateDownloadItem(string id, string orderItemId) + { + var (order, denied) = await LoadAuthorizedOrder(id); + if (denied != null) return denied; + + var orderItem = order.OrderItems.FirstOrDefault(x => x.Id == orderItemId) + ?? throw new ArgumentException("No order item found with the specified id"); + orderItem.IsDownloadActivated = !orderItem.IsDownloadActivated; + await orderService.UpdateOrder(order); + var model = new OrderModel(); + await orderViewModelService.PrepareOrderDetailsModel(model, order); + + await SaveSelectedTabIndex(persistForTheNextRequest: true); + return RedirectToAction("Edit", "Order", new { id }); + } + + #endregion + + #region License popup + + [PermissionAuthorizeAction(PermissionActionName.Edit)] + public async Task UploadLicenseFilePopup(string id, string orderItemId, + [FromServices] Grand.Business.Core.Interfaces.Catalog.Products.IProductService productService) + { + var (order, denied) = await LoadAuthorizedOrder(id); + if (denied != null) return denied; + + var orderItem = order.OrderItems.FirstOrDefault(x => x.Id == orderItemId) + ?? throw new ArgumentException("No order item found with the specified id"); + var product = await productService.GetProductByIdIncludeArch(orderItem.ProductId); + if (!product.IsDownload) throw new ArgumentException("Product is not downloadable"); + + var model = new OrderModel.UploadLicenseModel { + LicenseDownloadId = !string.IsNullOrEmpty(orderItem.LicenseDownloadId) ? orderItem.LicenseDownloadId : "", + OrderId = order.Id, + OrderItemId = orderItem.Id + }; + return View(model); + } + + [PermissionAuthorizeAction(PermissionActionName.Edit)] + [HttpPost] + public async Task UploadLicenseFilePopup(OrderModel.UploadLicenseModel model) + { + var (order, denied) = await LoadAuthorizedOrder(model.OrderId); + if (denied != null) return denied; + + var orderItem = order.OrderItems.FirstOrDefault(x => x.Id == model.OrderItemId) + ?? throw new ArgumentException("No order item found with the specified id"); + orderItem.LicenseDownloadId = !string.IsNullOrEmpty(model.LicenseDownloadId) ? model.LicenseDownloadId : null; + await orderService.UpdateOrder(order); + + model.RefreshPage = true; + return View(model); + } + + [PermissionAuthorizeAction(PermissionActionName.Edit)] + [HttpPost] + public async Task DeleteLicenseFilePopup(OrderModel.UploadLicenseModel model) + { + var (order, denied) = await LoadAuthorizedOrder(model.OrderId); + if (denied != null) return denied; + + var orderItem = order.OrderItems.FirstOrDefault(x => x.Id == model.OrderItemId) + ?? throw new ArgumentException("No order item found with the specified id"); + orderItem.LicenseDownloadId = null; + await orderService.UpdateOrder(order); + + return RedirectToAction("Edit", "Order", new { id = model.OrderId }); + } + + #endregion + + #region Add product to order + + [PermissionAuthorizeAction(PermissionActionName.Edit)] + public async Task AddProductToOrder(string orderId) + { + var (order, denied) = await LoadAuthorizedOrder(orderId); + if (denied != null) return denied; + + var model = await orderViewModelService.PrepareAddOrderProductModel(order); + return View(model); + } + + [PermissionAuthorizeAction(PermissionActionName.Edit)] + [HttpPost] + public async Task AddProductToOrder( + Grand.Web.Common.DataSource.DataSourceRequest command, OrderModel.AddOrderProductModel model, + [FromServices] Grand.Business.Core.Interfaces.Catalog.Products.IProductService productService) + { + var categoryIds = new List(); + if (!string.IsNullOrEmpty(model.SearchCategoryId)) categoryIds.Add(model.SearchCategoryId); + + var gridModel = new Grand.Web.Common.DataSource.DataSourceResult(); + var products = (await productService.SearchProducts(categoryIds: categoryIds, + storeId: scope.DefaultStoreId, + brandId: model.SearchBrandId, + collectionId: model.SearchCollectionId, + productType: model.SearchProductTypeId > 0 ? (Grand.Domain.Catalog.ProductType?)model.SearchProductTypeId : null, + keywords: model.SearchProductName, + pageIndex: command.Page - 1, + pageSize: command.PageSize, + showHidden: true)).products; + + gridModel.Data = products.Select(x => new OrderModel.AddOrderProductModel.ProductModel { + Id = x.Id, Name = x.Name, Sku = x.Sku + }); + gridModel.Total = products.TotalCount; + + return Json(gridModel); + } + + [PermissionAuthorizeAction(PermissionActionName.Edit)] + public async Task AddProductToOrderDetails(string orderId, string productId) + { + var (order, denied) = await LoadAuthorizedOrder(orderId); + if (denied != null) return denied; + + var model = await orderViewModelService.PrepareAddProductToOrderModel(order, productId); + return View(model); + } + + [PermissionAuthorizeAction(PermissionActionName.Edit)] + [HttpPost] + public async Task AddProductToOrderDetails(AddProductToOrderModel model) + { + var (order, denied) = await LoadAuthorizedOrder(model.OrderId); + if (denied != null) return denied; + + var warnings = await orderViewModelService.AddProductToOrderDetails(model); + if (!warnings.Any()) return RedirectToAction("Edit", "Order", new { id = model.OrderId }); + + var result = await orderViewModelService.PrepareAddProductToOrderModel(order, model.ProductId); + result.Warnings.AddRange(warnings); + return View(result); + } + + #endregion + + #region Addresses + + [PermissionAuthorizeAction(PermissionActionName.Preview)] + public async Task AddressEdit(string addressId, string orderId, bool billingAddress) + { + var (order, denied) = await LoadAuthorizedOrder(orderId); + if (denied != null) return denied; + + var address = new Grand.Domain.Common.Address(); + switch (billingAddress) + { + case true when order.BillingAddress != null: + if (order.BillingAddress.Id == addressId) address = order.BillingAddress; + break; + case false when order.ShippingAddress != null: + if (order.ShippingAddress.Id == addressId) address = order.ShippingAddress; + break; + } + + if (address == null) + throw new ArgumentException("No address found with the specified id", nameof(addressId)); + + var model = await orderViewModelService.PrepareOrderAddressModel(order, address); + model.BillingAddress = billingAddress; + return View(model); + } + + [PermissionAuthorizeAction(PermissionActionName.Edit)] + [HttpPost] + public async Task AddressEdit(OrderAddressModel model, + [FromServices] Grand.Business.Core.Interfaces.Common.Addresses.IAddressAttributeService addressAttributeService, + [FromServices] Grand.Business.Core.Interfaces.Common.Addresses.IAddressAttributeParser addressAttributeParser) + { + var (order, denied) = await LoadAuthorizedOrder(model.OrderId); + if (denied != null) return denied; + + var address = new Grand.Domain.Common.Address(); + switch (model.BillingAddress) + { + case true when order.BillingAddress != null: + if (order.BillingAddress.Id == model.Address.Id) address = order.BillingAddress; + break; + case false when order.ShippingAddress != null: + if (order.ShippingAddress.Id == model.Address.Id) address = order.ShippingAddress; + break; + } + + if (ModelState.IsValid) + { + var customAttributes = await model.Address.ParseCustomAddressAttributes(addressAttributeParser, addressAttributeService); + await orderViewModelService.UpdateOrderAddress(order, address, model, customAttributes); + return RedirectToAction("AddressEdit", + new { addressId = model.Address.Id, orderId = model.OrderId, model.BillingAddress }); + } + + model = await orderViewModelService.PrepareOrderAddressModel(order, address); + return View(model); + } + + #endregion + + #region Order notes + + [PermissionAuthorizeAction(PermissionActionName.List)] + [HttpPost] + public async Task OrderNotesSelect(string orderId, Grand.Web.Common.DataSource.DataSourceRequest command) + { + var order = await orderService.GetOrderById(orderId) + ?? throw new ArgumentException("No order found with the specified id"); + // Preserved host divergence: Admin's original throws for both not-found and Sales-Manager + // denial; Store's original throws only for not-found and soft-denies (empty content) for + // store-mismatch. Unifying these into one behavior would be a real change for one host - + // deliberately not done here. See plan's Global Constraints. + if (!await scope.HasAccess(order)) return Content(""); + + var orderNoteModels = await orderViewModelService.PrepareOrderNotes(order); + var gridModel = new Grand.Web.Common.DataSource.DataSourceResult { + Data = orderNoteModels, + Total = orderNoteModels.Count + }; + return Json(gridModel); + } + + [PermissionAuthorizeAction(PermissionActionName.Edit)] + public async Task OrderNoteAdd(string orderId, string downloadId, bool displayToCustomer, string message) + { + var order = await orderService.GetOrderById(orderId); + if (order == null || !await scope.HasAccess(order)) return Json(new { Result = false }); + + await orderViewModelService.InsertOrderNote(order, downloadId, displayToCustomer, message); + return Json(new { Result = true }); + } + + [PermissionAuthorizeAction(PermissionActionName.Delete)] + [HttpPost] + public async Task OrderNoteDelete(string id, string orderId) + { + var order = await orderService.GetOrderById(orderId) + ?? throw new ArgumentException("No order found with the specified id"); + if (!await scope.HasAccess(order)) return Json(new { Result = false }); + + await orderViewModelService.DeleteOrderNote(order, id); + return new JsonResult(""); + } + + #endregion +} diff --git a/src/Web/Grand.Web.AdminShared/Grand.Web.AdminShared.csproj b/src/Web/Grand.Web.AdminShared/Grand.Web.AdminShared.csproj index 88786f07b..59d418e9a 100644 --- a/src/Web/Grand.Web.AdminShared/Grand.Web.AdminShared.csproj +++ b/src/Web/Grand.Web.AdminShared/Grand.Web.AdminShared.csproj @@ -9,6 +9,7 @@ + diff --git a/src/Web/Grand.Web.AdminShared/Interfaces/IAdminDataScope.cs b/src/Web/Grand.Web.AdminShared/Interfaces/IAdminDataScope.cs index b9fda2ef5..9310c9829 100644 --- a/src/Web/Grand.Web.AdminShared/Interfaces/IAdminDataScope.cs +++ b/src/Web/Grand.Web.AdminShared/Interfaces/IAdminDataScope.cs @@ -50,4 +50,15 @@ public interface IAdminDataScope /// `ResourceKeyPrefix != "Vendor"` check in ProductViewModelService, which overloaded a /// localization-key property for behavior gating. bool CanFeatureOnHomepage { get; } + + /// Order line items visible to the current host — e.g. Vendor sees only its own + /// items within a mixed-vendor order. Identity (no filtering) for hosts with no such + /// restriction (Admin, Store, and every non-Order entity). Only + /// overrides this. Lives on + /// the shared interface rather than a separate Order-only interface because + /// IAdminDataScope<TEntity> is already the single per-host strategy object injected + /// into BaseOrderController/OrderViewModelService — see ARCH-001 Order consolidation spec + /// §3.4. + IEnumerable FilterOrderItems( + IEnumerable orderItems) => orderItems; } diff --git a/src/Web/Grand.Web.AdminShared/Services/AdminOrderDataScope.cs b/src/Web/Grand.Web.AdminShared/Services/AdminOrderDataScope.cs new file mode 100644 index 000000000..e119626ef --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Services/AdminOrderDataScope.cs @@ -0,0 +1,33 @@ +#nullable enable + +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Domain.Orders; +using Grand.Infrastructure; +using Grand.Web.AdminShared.Interfaces; + +namespace Grand.Web.AdminShared.Services; + +/// +/// Admin's . Deliberately NOT the generic +/// — Admin's original OrderController gates +/// nearly every action through a Sales Manager check +/// (groupService.IsSalesManager(CurrentCustomer) && CurrentCustomer.SeId != +/// order.SeId) that Store and Vendor never had. Reusing the always-true generic scope here +/// would silently drop that restriction. See ARCH-001 Order consolidation spec §3.2. +/// +public class AdminOrderDataScope(IContextAccessor contextAccessor, IGroupService groupService) + : IAdminDataScope +{ + public async Task HasAccess(Order entity) + { + if (entity is null) return false; + var isSalesManager = await groupService.IsSalesManager(contextAccessor.WorkContext.CurrentCustomer); + return !isSalesManager || contextAccessor.WorkContext.CurrentCustomer.SeId == entity.SeId; + } + + public string? DefaultStoreId => null; + public string ResourceKeyPrefix => "Admin"; + public bool ShowStoreSelector => true; + public string? DefaultVendorId => null; + public bool CanFeatureOnHomepage => true; // unused for Order; required interface member +} diff --git a/src/Web/Grand.Web.AdminShared/Services/OrderViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/OrderViewModelService.cs index 3b86f2975..667009634 100644 --- a/src/Web/Grand.Web.AdminShared/Services/OrderViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/OrderViewModelService.cs @@ -77,6 +77,7 @@ public class OrderViewModelService : IOrderViewModelService private readonly IOrderStatusService _orderStatusService; private readonly IMediator _mediator; private readonly IEnumTranslationService _enumTranslationService; + private readonly IAdminDataScope _scope; #endregion @@ -116,7 +117,8 @@ public OrderViewModelService(IOrderService orderService, IOrderTagService orderTagService, IOrderStatusService orderStatusService, IMediator mediator, - IProductAttributeFormatter productAttributeFormatter, IEnumTranslationService enumTranslationService) + IProductAttributeFormatter productAttributeFormatter, IEnumTranslationService enumTranslationService, + IAdminDataScope scope) { _orderService = orderService; _pricingService = priceCalculationService; @@ -154,6 +156,7 @@ public OrderViewModelService(IOrderService orderService, _mediator = mediator; _productAttributeFormatter = productAttributeFormatter; _enumTranslationService = enumTranslationService; + _scope = scope; } #endregion @@ -345,62 +348,80 @@ public virtual async Task PrepareOrderDetailsModel(OrderModel model, Order order model.OrderGuid = order.OrderGuid; var status = await _orderStatusService.GetAll(); - model.OrderStatuses = - status.Select(x => new SelectListItem { Value = x.StatusId.ToString(), Text = x.Name }).ToList(); + if (_scope.DefaultVendorId is null) + model.OrderStatuses = + status.Select(x => new SelectListItem { Value = x.StatusId.ToString(), Text = x.Name }).ToList(); model.OrderStatus = status.FirstOrDefault(x => x.StatusId == order.OrderStatusId)?.Name; var store = await _storeService.GetStoreById(order.StoreId); model.StoreName = store != null ? store.Shortcut : "Unknown"; - model.CustomerId = order.CustomerId; + if (_scope.DefaultVendorId is null) + model.CustomerId = order.CustomerId; model.UserFields = order.UserFields; var customer = await _customerService.GetCustomerById(order.CustomerId); if (customer != null) model.CustomerInfo = !string.IsNullOrEmpty(customer.Email) ? customer.Email - : _translationService.GetResource("Admin.Customers.Guest"); + : _translationService.GetResource($"{_scope.ResourceKeyPrefix}.Customers.Guest"); - model.CustomerIp = order.CustomerIp; + if (_scope.DefaultVendorId is null) + model.CustomerIp = order.CustomerIp; model.VatNumber = order.VatNumber; model.CreatedOn = _dateTimeService.ConvertToUserTime(order.CreatedOnUtc, DateTimeKind.Utc); model.TaxDisplayType = _taxSettings.TaxDisplayType; - if (!string.IsNullOrEmpty(order.AffiliateId)) + if (_scope.DefaultVendorId is null) { - var affiliate = await _affiliateService.GetAffiliateById(order.AffiliateId); - if (affiliate != null) + if (!string.IsNullOrEmpty(order.AffiliateId)) { - model.AffiliateId = affiliate.Id; - model.AffiliateName = affiliate.GetFullName(); + var affiliate = await _affiliateService.GetAffiliateById(order.AffiliateId); + if (affiliate != null) + { + model.AffiliateId = affiliate.Id; + model.AffiliateName = affiliate.GetFullName(); + } } } - if (!string.IsNullOrEmpty(order.SeId)) + if (_scope.DefaultVendorId is null) { - var salesEmployee = await _salesEmployeeService.GetSalesEmployeeById(order.SeId); - if (salesEmployee != null) + if (!string.IsNullOrEmpty(order.SeId)) { - model.SalesEmployeeId = salesEmployee.Id; - model.SalesEmployeeName = salesEmployee.Name; + var salesEmployee = await _salesEmployeeService.GetSalesEmployeeById(order.SeId); + if (salesEmployee != null) + { + model.SalesEmployeeId = salesEmployee.Id; + model.SalesEmployeeName = salesEmployee.Name; + } } } //order's tags - if (order.OrderTags.Any()) + if (_scope.DefaultVendorId is null) { - var tagsName = new List(); - foreach (var item in order.OrderTags) + if (order.OrderTags.Any()) { - var tag = await _orderTagService.GetOrderTagById(item); - if (tag != null) - tagsName.Add(tag.Name); - } + var tagsName = new List(); + foreach (var item in order.OrderTags) + { + var tag = await _orderTagService.GetOrderTagById(item); + if (tag != null) + tagsName.Add(tag.Name); + } - model.OrderTags = string.Join(",", tagsName); + model.OrderTags = string.Join(",", tagsName); + } } + model.CurrencyRate = order.CurrencyRate; + model.CurrencyCode = order.CustomerCurrencyCode; + #region Order totals + // primaryStoreCurrency/orderCurrency stay ungated: they're needed below (outside this + // gate) to format each OrderItemModel's unit price/discount/subtotal/commission, which + // Vendor's own OrderDetails.Products partial does render. var primaryStoreCurrency = await _currencyService.GetCurrencyByCode(order.PrimaryCurrencyCode) ?? await _currencyService.GetCurrencyById(_currencySettings.PrimaryStoreCurrencyId); @@ -411,6 +432,15 @@ public virtual async Task PrepareOrderDetailsModel(OrderModel model, Order order if (orderCurrency == null) throw new Exception("Cannot load order currency"); + // Vendor's original service never populated the rest of this block (subtotal, discount, + // shipping, tax, payment fee, order total, refund, loyalty points, gift vouchers, + // discount usage, profit) - matches the gating already applied above for CustomerId, + // CustomerIp, Affiliate, SalesEmployee, OrderTags and OrderStatuses (final review I2). + // None of Vendor's own Order partials render these fields today, so this has no visible + // behavior change for Vendor - it closes a latent exposure risk (e.g. a future shared + // view rendering merchant Profit to a vendor). + if (_scope.DefaultVendorId is null) + { //subtotal model.OrderSubtotalInclTax = _priceFormatter.FormatPrice(order.OrderSubtotalInclTax, orderCurrency); model.OrderSubtotalExclTax = _priceFormatter.FormatPrice(order.OrderSubtotalExclTax, orderCurrency); @@ -488,8 +518,6 @@ public virtual async Task PrepareOrderDetailsModel(OrderModel model, Order order //total model.OrderTotal = _priceFormatter.FormatPrice(order.OrderTotal, orderCurrency); model.OrderTotalValue = order.OrderTotal; - model.CurrencyRate = order.CurrencyRate; - model.CurrencyCode = order.CustomerCurrencyCode; //refunded amount if (order.RefundedAmount > 0) @@ -564,6 +592,7 @@ public virtual async Task PrepareOrderDetailsModel(OrderModel model, Order order } #endregion + } #region Payment info @@ -572,12 +601,18 @@ public virtual async Task PrepareOrderDetailsModel(OrderModel model, Order order model.PaymentMethod = pm != null ? pm.FriendlyName : order.PaymentMethodSystemName; model.PaymentStatus = _enumTranslationService.GetTranslationEnum(order.PaymentStatusId); model.PaymentStatusEnum = order.PaymentStatusId; - var pt = await _paymentTransactionService.GetOrderByGuid(order.OrderGuid); - if (pt != null) - model.PaymentTransactionId = pt.Id; - model.PrimaryStoreCurrencyCode = order.PrimaryCurrencyCode; - model.MaxAmountToRefund = order.OrderTotal - order.RefundedAmount; + // Vendor's original service never populated PaymentTransactionId, PrimaryStoreCurrencyCode + // or MaxAmountToRefund either - same gate as the totals block above (final review I2). + if (_scope.DefaultVendorId is null) + { + var pt = await _paymentTransactionService.GetOrderByGuid(order.OrderGuid); + if (pt != null) + model.PaymentTransactionId = pt.Id; + + model.PrimaryStoreCurrencyCode = order.PrimaryCurrencyCode; + model.MaxAmountToRefund = order.OrderTotal - order.RefundedAmount; + } #endregion @@ -614,7 +649,10 @@ await _addressAttributeParser.FormatAttributes(_contextAccessor.WorkContext.Work model.BillingAddress.FaxRequired = _addressSettings.FaxRequired; model.BillingAddress.NoteEnabled = _addressSettings.NoteEnabled; - model.ShippingStatus = _enumTranslationService.GetTranslationEnum(order.ShippingStatusId); + // Vendor's original service never populated ShippingStatus - same gate as the totals + // block above (final review I2). + if (_scope.DefaultVendorId is null) + model.ShippingStatus = _enumTranslationService.GetTranslationEnum(order.ShippingStatusId); if (order.ShippingStatusId != ShippingStatus.ShippingNotRequired) { model.IsShippable = true; @@ -693,7 +731,7 @@ await _addressAttributeParser.FormatAttributes(_contextAccessor.WorkContext.Work model.CheckoutAttributeInfo = order.CheckoutAttributeDescription; var hasDownloadableItems = false; - var products = order.OrderItems; + var products = _scope.FilterOrderItems(order.OrderItems); foreach (var orderItem in products) { var product = await _productService.GetProductByIdIncludeArch(orderItem.ProductId); diff --git a/src/Web/Grand.Web.AdminShared/Services/RoutedOrderDataScope.cs b/src/Web/Grand.Web.AdminShared/Services/RoutedOrderDataScope.cs new file mode 100644 index 000000000..108e09e7a --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Services/RoutedOrderDataScope.cs @@ -0,0 +1,53 @@ +#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) loads all +/// three hosts 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. +/// +/// First 3-branch routed scope in ARCH-001 — every prior entity's Vendor branch either didn't +/// exist (Category/Collection) or reused the same scope shape as Product's. Order genuinely +/// needs all three. +/// +public class RoutedOrderDataScope( + IHttpContextAccessor httpContextAccessor, + AdminOrderDataScope adminScope, + StoreOrderDataScope storeScope, + VendorOrderDataScope 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/Sales-Manager tenant isolation, so an + //unrecognized or missing area must never silently resolve to any concrete scope + _ => throw new InvalidOperationException( + $"RoutedOrderDataScope: unrecognized or missing area '{area}'.") + }; + } + } + + public Task HasAccess(Order entity) => Resolved.HasAccess(entity); + public Task CanView(Order entity) => Resolved.CanView(entity); + public IEnumerable FilterOrderItems(IEnumerable orderItems) => + Resolved.FilterOrderItems(orderItems); + 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/StoreOrderDataScope.cs b/src/Web/Grand.Web.AdminShared/Services/StoreOrderDataScope.cs new file mode 100644 index 000000000..23e762a07 --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Services/StoreOrderDataScope.cs @@ -0,0 +1,28 @@ +#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 +/// : Order is a plain +/// with a single StoreId field, not IStoreLinkEntity (no Stores/ +/// LimitedToStores list), so the generic class's where TEntity : BaseEntity, +/// IStoreLinkEntity constraint doesn't apply. Mirrors Store's original controller's +/// order.StoreId != StaffStoreId check, repeated at every action site in that file. +/// +public class StoreOrderDataScope(IContextAccessor contextAccessor) : IAdminDataScope +{ + public Task HasAccess(Order 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; +} diff --git a/src/Web/Grand.Web.AdminShared/Services/VendorOrderDataScope.cs b/src/Web/Grand.Web.AdminShared/Services/VendorOrderDataScope.cs new file mode 100644 index 000000000..564e9eca0 --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Services/VendorOrderDataScope.cs @@ -0,0 +1,33 @@ +#nullable enable + +using Grand.Domain.Orders; +using Grand.Infrastructure; +using Grand.Web.AdminShared.Interfaces; + +namespace Grand.Web.AdminShared.Services; + +/// +/// Vendor's . Bespoke: ownership is over a child +/// collection (any OrderItem.VendorId match), not a flat field on the entity itself — +/// ports the existing HasAccessToOrder/HasAccessToOrderItem extension methods +/// from Grand.Web.Vendor/Extensions/HasAccess.cs. Also the only scope that overrides +/// : a vendor viewing a mixed-vendor order sees only its own +/// line items, ported from Vendor's original +/// order.OrderItems.Where(HasAccessToOrderItem) filter inside +/// PrepareOrderDetailsModel. +/// +public class VendorOrderDataScope(IContextAccessor contextAccessor) : IAdminDataScope +{ + public Task HasAccess(Order entity) => + Task.FromResult(entity is not null && + entity.OrderItems.Any(i => i.VendorId == contextAccessor.WorkContext.CurrentVendor.Id)); + + public IEnumerable FilterOrderItems(IEnumerable orderItems) => + orderItems.Where(i => i.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 bd84eb0ac..cd4355fcc 100644 --- a/src/Web/Grand.Web.AdminShared/Startup/StartupApplication.cs +++ b/src/Web/Grand.Web.AdminShared/Startup/StartupApplication.cs @@ -1,6 +1,7 @@ using elFinder.Net.AspNetCore.Extensions; using elFinder.Net.Drivers.FileSystem.Extensions; using Grand.Domain.Catalog; +using Grand.Domain.Orders; using Grand.Infrastructure; using Grand.Web.AdminShared.Interfaces; using Grand.Web.AdminShared.Services; @@ -82,6 +83,13 @@ public void ConfigureServices(IServiceCollection services, IConfiguration config services.AddScoped>(); services.AddScoped>(); services.AddScoped, RoutedCollectionDataScope>(); + + // IAdminDataScope: three bespoke implementations, none reusing the generic Global/Store + // scopes — see AdminOrderDataScope/StoreOrderDataScope/VendorOrderDataScope doc comments. + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped, RoutedOrderDataScope>(); } public void Configure(WebApplication application, IWebHostEnvironment webHostEnvironment) diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/AddProductToOrder.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/AddProductToOrder.cshtml similarity index 94% rename from src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/AddProductToOrder.cshtml rename to src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/AddProductToOrder.cshtml index 5edc6226c..1ad923342 100644 --- a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/AddProductToOrder.cshtml +++ b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/AddProductToOrder.cshtml @@ -1,8 +1,9 @@ -@model OrderModel.AddOrderProductModel +@model OrderModel.AddOrderProductModel @inject AdminAreaSettings adminAreaSettings @{ //page title ViewBag.Title = string.Format(Loc["Admin.Orders.Products.AddNew.Title1"], Model.OrderNumber); + var area = ViewContext.RouteData.Values["area"]?.ToString(); }
@@ -19,7 +20,7 @@
- +
- +
@Loc["Admin.Orders.Products.AddNew.Note1"] @@ -89,7 +90,7 @@ dataSource: { transport: { read: { - url: "@Html.Raw(Url.Action("AddProductToOrder", "Order", new { area = Constants.AreaAdmin }))", + url: "@Html.Raw(Url.Action("AddProductToOrder", "Order", new { area = area }))", type: "POST", dataType: "json", data: additionalData @@ -168,11 +169,11 @@ var selectedItem = grid.dataItem(grid.select()); var productId = selectedItem.Id; //load "product details page" block - var productDetailsActionUrl = '@Html.Raw(Url.Action("AddProductToOrderDetails", "Order", new { orderId = Model.OrderId, productId = "productidplaceholder", area = Constants.AreaAdmin }))'; + var productDetailsActionUrl = '@Html.Raw(Url.Action("AddProductToOrderDetails", "Order", new { orderId = Model.OrderId, productId = "productidplaceholder", area = area }))'; productDetailsActionUrl = productDetailsActionUrl.replace("productidplaceholder", productId); setLocation(productDetailsActionUrl); }
- \ No newline at end of file + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/AddProductToOrderDetails.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/AddProductToOrderDetails.cshtml similarity index 93% rename from src/Web/Grand.Web.Store/Areas/Store/Views/Order/AddProductToOrderDetails.cshtml rename to src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/AddProductToOrderDetails.cshtml index e39f7868b..0eb4bd467 100644 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/AddProductToOrderDetails.cshtml +++ b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/AddProductToOrderDetails.cshtml @@ -1,11 +1,12 @@ -@model OrderModel.AddOrderProductModel.ProductDetailsModel +@model OrderModel.AddOrderProductModel.ProductDetailsModel @{ ViewBag.Title = string.Format(Loc["Admin.Orders.Products.AddNew.Title2"], Model.Name, Model.OrderNumber); + var area = ViewContext.RouteData.Values["area"]?.ToString(); } -
- +
@@ -107,5 +108,5 @@
- - \ No newline at end of file + + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/AddressEdit.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/AddressEdit.cshtml similarity index 84% rename from src/Web/Grand.Web.Store/Areas/Store/Views/Order/AddressEdit.cshtml rename to src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/AddressEdit.cshtml index 7516132fe..186c120c8 100644 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/AddressEdit.cshtml +++ b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/AddressEdit.cshtml @@ -1,9 +1,10 @@ -@model OrderAddressModel +@model OrderAddressModel @{ //page title ViewBag.Title = Loc["Admin.Orders.Address.EditAddress"]; + var area = ViewContext.RouteData.Values["area"]?.ToString(); } -
@@ -24,7 +25,7 @@ - +
@@ -33,4 +34,4 @@
- \ No newline at end of file + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Edit.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/Edit.cshtml similarity index 93% rename from src/Web/Grand.Web.Store/Areas/Store/Views/Order/Edit.cshtml rename to src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/Edit.cshtml index 12951625a..5a58f98a8 100644 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Edit.cshtml +++ b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/Edit.cshtml @@ -1,4 +1,4 @@ -@using Grand.Business.Core.Interfaces.Common.Security +@using Grand.Business.Core.Interfaces.Common.Security @using Grand.Domain.Permissions @model OrderModel @inject IPermissionService permissionService @@ -8,8 +8,9 @@ //has "Manage Documents" permission? var canManageDocuments = await permissionService.Authorize(StandardPermission.ManageDocuments); var canManageMessageQueue = await permissionService.Authorize(StandardPermission.ManageMessageQueue); + var area = ViewContext.RouteData.Values["area"]?.ToString(); } -
+
@@ -25,7 +26,7 @@
- + @Loc["Admin.Orders.PdfInvoice"] @@ -44,7 +45,7 @@ }); }); - +
@@ -122,7 +123,7 @@ } - + @@ -130,4 +131,4 @@ - \ No newline at end of file + diff --git a/src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/Partials/CreateOrUpdateAddress.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/Partials/CreateOrUpdateAddress.cshtml new file mode 100644 index 000000000..4fea0e621 --- /dev/null +++ b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/Partials/CreateOrUpdateAddress.cshtml @@ -0,0 +1,5 @@ +@model OrderAddressModel + +
+ + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/OrderDetails.Addresses.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/Partials/OrderDetails.Addresses.cshtml similarity index 97% rename from src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/OrderDetails.Addresses.cshtml rename to src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/Partials/OrderDetails.Addresses.cshtml index ed94e9aac..8707d0dca 100644 --- a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/OrderDetails.Addresses.cshtml +++ b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/Partials/OrderDetails.Addresses.cshtml @@ -1,5 +1,8 @@ -@model OrderModel - +@model OrderModel +@{ + var area = ViewContext.RouteData.Values["area"]?.ToString(); +} +

@Loc["Admin.Orders.BillingInfo"]

@@ -148,7 +151,7 @@ -  @Loc["Admin.Common.Edit"] +  @Loc["Admin.Common.Edit"] @@ -299,7 +302,7 @@ -  @Loc["Admin.Common.Edit"] +  @Loc["Admin.Common.Edit"] @@ -434,4 +437,4 @@ else
} - \ No newline at end of file + \ No newline at end of file diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/OrderDetails.Documents.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/Partials/OrderDetails.Documents.cshtml similarity index 79% rename from src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/OrderDetails.Documents.cshtml rename to src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/Partials/OrderDetails.Documents.cshtml index e8373571a..60f4c269c 100644 --- a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/OrderDetails.Documents.cshtml +++ b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/Partials/OrderDetails.Documents.cshtml @@ -1,14 +1,17 @@ -@model OrderModel +@model OrderModel @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/Order/Partials/OrderDetails.Info.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/Partials/OrderDetails.Info.cshtml similarity index 97% rename from src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/OrderDetails.Info.cshtml rename to src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/Partials/OrderDetails.Info.cshtml index 29d05e7dd..d180b060e 100644 --- a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/OrderDetails.Info.cshtml +++ b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/Partials/OrderDetails.Info.cshtml @@ -1,4 +1,4 @@ -@using System.Text.Encodings.Web +@using System.Text.Encodings.Web @using Grand.Business.Core.Interfaces.Checkout.Orders @using Grand.Domain.Payments @using Grand.Domain.Tax @@ -29,6 +29,7 @@ } orderTagsSB.Append("]"); + var area = ViewContext.RouteData.Values["area"]?.ToString(); }
- + @@ -408,7 +409,7 @@ { var discount = Model.UsedDiscounts[i];
- + \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/OrderDetails.Notes.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/Partials/OrderDetails.Notes.cshtml similarity index 92% rename from src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/OrderDetails.Notes.cshtml rename to src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/Partials/OrderDetails.Notes.cshtml index a04d4b4f8..9c482efbb 100644 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/OrderDetails.Notes.cshtml +++ b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/Partials/OrderDetails.Notes.cshtml @@ -1,15 +1,16 @@ -@using Grand.Domain.Media +@using Grand.Domain.Media @model OrderModel @{ ViewData["DownloadType"] = DownloadType.Order; 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/Order/Partials/OrderDetails.Products.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/Partials/OrderDetails.Products.cshtml similarity index 95% rename from src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/OrderDetails.Products.cshtml rename to src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/Partials/OrderDetails.Products.cshtml index f0adac689..f5ee393c1 100644 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/OrderDetails.Products.cshtml +++ b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/Partials/OrderDetails.Products.cshtml @@ -1,7 +1,10 @@ -@using Grand.Domain.Tax +@using Grand.Domain.Tax @model OrderModel +@{ + var area = ViewContext.RouteData.Values["area"]?.ToString(); +} - +
@@ -95,7 +98,7 @@
- @item.ProductName + @item.ProductName @if (!string.IsNullOrEmpty(item.AttributeInfo)) { @@ -135,7 +138,7 @@ @Loc["Admin.Orders.Products.MerchandiseReturns"]: @for (var i = 0; i < item.MerchandiseReturnIds.Count; i++) { -  @Loc["Admin.Orders.MerchandiseReturns.View"] +  @Loc["Admin.Orders.MerchandiseReturns.View"] if (i != item.MerchandiseReturnIds.Count - 1) { , @@ -149,7 +152,7 @@ @Loc["Admin.Orders.Products.GiftVouchers"]: @for (var i = 0; i < item.PurchasedGiftVoucherIds.Count; i++) { -  @Loc["Admin.Orders.Products.GiftVoucher.View"] +  @Loc["Admin.Orders.Products.GiftVoucher.View"] if (i != item.PurchasedGiftVoucherIds.Count - 1) { , @@ -185,10 +188,10 @@
@if (item.LicenseDownloadGuid != Guid.Empty) { - @Loc["Admin.Orders.Products.License.DownloadLicense"] + @Loc["Admin.Orders.Products.License.DownloadLicense"] }
- + @Loc["Admin.Orders.Products.License.UploadFile"]
@@ -342,7 +345,7 @@
- + @if (!string.IsNullOrEmpty(Model.CheckoutAttributeInfo)) { @@ -351,5 +354,5 @@ }
- +
\ No newline at end of file diff --git a/src/Web/Grand.Web.Vendor/Areas/Vendor/Views/Order/Partials/OrderDetails.Shipment.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/Partials/OrderDetails.Shipment.cshtml similarity index 71% rename from src/Web/Grand.Web.Vendor/Areas/Vendor/Views/Order/Partials/OrderDetails.Shipment.cshtml rename to src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/Partials/OrderDetails.Shipment.cshtml index e38ab5628..185ea0f46 100644 --- a/src/Web/Grand.Web.Vendor/Areas/Vendor/Views/Order/Partials/OrderDetails.Shipment.cshtml +++ b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/Partials/OrderDetails.Shipment.cshtml @@ -1,28 +1,34 @@ -@using Grand.Business.Core.Interfaces.Common.Security +@using Grand.Business.Core.Interfaces.Common.Security @using Grand.Domain.Permissions +@using Grand.Domain.Orders @model OrderModel @inject IPermissionService permissionService; - +@inject IAdminDataScope Scope +@{ + var area = ViewContext.RouteData.Values["area"]?.ToString(); + var prefix = Scope.ResourceKeyPrefix; +} + @if (Model.IsShippable) { @if (await permissionService.Authorize(PermissionSystemName.Shipments)) {
-

@Loc["Vendor.Orders.Shipments"]

+

@Loc[$"{prefix}.Orders.Shipments"]

- +
- +
@@ -35,7 +41,7 @@ dataSource: { transport: { read: { - url: "@Html.Raw(Url.Action("ShipmentsByOrder", "Shipment", new { orderId = Model.Id, area = Constants.AreaVendor }))", + url: "@Html.Raw(Url.Action("ShipmentsByOrder", "Shipment", new { orderId = Model.Id, area = area }))", type: "POST", dataType: "json", data: addAntiForgeryToken @@ -67,27 +73,27 @@ scrollable: false, columns: [{ field: "ShipmentNumber", - title: "@Loc["Vendor.Orders.Shipments.ID"]", + title: "@Loc[$"{prefix}.Orders.Shipments.ID"]", width: 50, - template: '#=ShipmentNumber#' + template: '#=ShipmentNumber#' }, { field: "TrackingNumber", - title: "@Loc["Vendor.Orders.Shipments.TrackingNumber"]", + title: "@Loc[$"{prefix}.Orders.Shipments.TrackingNumber"]", width: 100, - template: '#=kendo.htmlEncode(TrackingNumber)#' + template: '#=kendo.htmlEncode(TrackingNumber)#' }, { field: "TotalWeight", - title: "@Loc["Vendor.Orders.Shipments.TotalWeight"]", + title: "@Loc[$"{prefix}.Orders.Shipments.TotalWeight"]", width: 100 }, { field: "ShippedDate", - title: "@Loc["Vendor.Orders.Shipments.ShippedDate"]", + title: "@Loc[$"{prefix}.Orders.Shipments.ShippedDate"]", width: 200, type: "date", format: "{0:G}" }, { field: "DeliveryDate", - title: "@Loc["Vendor.Orders.Shipments.DeliveryDate"]", + title: "@Loc[$"{prefix}.Orders.Shipments.DeliveryDate"]", width: 200, type: "date", format: "{0:G}" @@ -101,7 +107,7 @@ dataSource: { transport: { read: { - url: "@Html.Raw(Url.Action("ShipmentsItemsByShipmentId", "Shipment", new { area = Constants.AreaVendor }))?shipmentId="+e.data.Id, + url: "@Html.Raw(Url.Action("ShipmentsItemsByShipmentId", "Shipment", new { area = area }))?shipmentId="+e.data.Id, type: "POST", dataType: "json", data: addAntiForgeryToken @@ -132,23 +138,23 @@ columns: [ { field: "ProductName", - title: "@Loc["Vendor.Orders.Shipments.Products.ProductName"]", + title: "@Loc[$"{prefix}.Orders.Shipments.Products.ProductName"]", width: 400 },{ field: "ShippedFromWarehouse", - title: "@Loc["Vendor.Orders.Shipments.Products.Warehouse"]", + title: "@Loc[$"{prefix}.Orders.Shipments.Products.Warehouse"]", width: 150 },{ field: "QuantityInThisShipment", - title: "@Loc["Vendor.Orders.Shipments.Products.QtyShipped"]", + title: "@Loc[$"{prefix}.Orders.Shipments.Products.QtyShipped"]", width: 150 },{ field: "ItemWeight", - title: "@Loc["Vendor.Orders.Shipments.Products.ItemWeight"]", + title: "@Loc[$"{prefix}.Orders.Shipments.Products.ItemWeight"]", width: 150 },{ field: "ItemDimensions", - title: "@Loc["Vendor.Orders.Shipments.Products.ItemDimensions"]", + title: "@Loc[$"{prefix}.Orders.Shipments.Products.ItemDimensions"]", width: 150 } ] @@ -160,8 +166,8 @@ else {
-
@Loc["Vendor.Orders.ShippingInfo.NotRequired"]
+
@Loc[$"{prefix}.Orders.ShippingInfo.NotRequired"]
} - \ No newline at end of file + diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/ProductAddAttributes.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/Partials/ProductAddAttributes.cshtml similarity index 100% rename from src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/ProductAddAttributes.cshtml rename to src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/Partials/ProductAddAttributes.cshtml diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/ProductAddGiftVoucherInfo.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/Partials/ProductAddGiftVoucherInfo.cshtml similarity index 100% rename from src/Web/Grand.Web.Admin/Areas/Admin/Views/Order/Partials/ProductAddGiftVoucherInfo.cshtml rename to src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/Partials/ProductAddGiftVoucherInfo.cshtml diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/UploadLicenseFilePopup.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/UploadLicenseFilePopup.cshtml similarity index 86% rename from src/Web/Grand.Web.Store/Areas/Store/Views/Order/UploadLicenseFilePopup.cshtml rename to src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/UploadLicenseFilePopup.cshtml index 5ed1dfa16..7dfae290c 100644 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/UploadLicenseFilePopup.cshtml +++ b/src/Web/Grand.Web.AdminShared/Views/AdminShared/Order/UploadLicenseFilePopup.cshtml @@ -1,10 +1,11 @@ -@model OrderModel.UploadLicenseModel +@model OrderModel.UploadLicenseModel @{ Layout = ""; //page title ViewBag.Title = Loc["Admin.Orders.Products.License.UploadFile"]; + var area = ViewContext.RouteData.Values["area"]?.ToString(); } -
+
@@ -22,7 +23,7 @@ }
- +
@@ -61,4 +62,4 @@ }); }); - \ No newline at end of file + diff --git a/src/Web/Grand.Web.AdminShared/Views/AdminShared/_ViewImports.cshtml b/src/Web/Grand.Web.AdminShared/Views/AdminShared/_ViewImports.cshtml index 99532cee9..6f40614b8 100644 --- a/src/Web/Grand.Web.AdminShared/Views/AdminShared/_ViewImports.cshtml +++ b/src/Web/Grand.Web.AdminShared/Views/AdminShared/_ViewImports.cshtml @@ -14,6 +14,7 @@ @using Grand.Web.Common.Extensions @using Grand.Web.Common.Localization @using Grand.Web.AdminShared.Models.Catalog +@using Grand.Web.AdminShared.Models.Orders @using Grand.Web.AdminShared.Interfaces @inject LocService Loc diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/AddProductToOrder.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/AddProductToOrder.cshtml deleted file mode 100644 index d454b6b3a..000000000 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/AddProductToOrder.cshtml +++ /dev/null @@ -1,178 +0,0 @@ -@model OrderModel.AddOrderProductModel -@inject AdminAreaSettings adminAreaSettings -@{ - //page title - ViewBag.Title = string.Format(Loc["Admin.Orders.Products.AddNew.Title1"], Model.OrderNumber); -} - -
-
-
-
-
- - @string.Format(Loc["Admin.Orders.Products.AddNew.Title1"], Model.OrderNumber) - - - @Html.ActionLink(Loc["Admin.Orders.Products.AddNew.BackToOrder"], "Edit", new { id = Model.OrderId }) - -
-
-
- -
-
- -
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
-
- -
-
- @Loc["Admin.Orders.Products.AddNew.Note1"] -
-
-
-
-
- - -
-
- \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/CreateOrUpdateAddress.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/CreateOrUpdateAddress.cshtml deleted file mode 100644 index 02aab8b36..000000000 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/CreateOrUpdateAddress.cshtml +++ /dev/null @@ -1,5 +0,0 @@ -@model OrderAddressModel - -
- - \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/OrderDetails.Addresses.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/OrderDetails.Addresses.cshtml deleted file mode 100644 index 10359b8b4..000000000 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/OrderDetails.Addresses.cshtml +++ /dev/null @@ -1,437 +0,0 @@ -@model OrderModel - -
-
-

@Loc["Admin.Orders.BillingInfo"]

-

- - @if (Model.BillingAddress.FirstNameEnabled || Model.BillingAddress.LastNameEnabled) - { - - - - - } - @if (Model.BillingAddress.EmailEnabled) - { - - - - - } - @if (Model.BillingAddress.PhoneEnabled) - { - - - - - } - @if (Model.BillingAddress.FaxEnabled) - { - - - - - } - @if (Model.BillingAddress.CompanyEnabled) - { - - - - - } - @if (Model.BillingAddress.VatNumberEnabled) - { - - - - - } - @if (Model.BillingAddress.StreetAddressEnabled) - { - - - - - } - @if (Model.BillingAddress.StreetAddress2Enabled) - { - - - - - } - @if (Model.BillingAddress.CityEnabled) - { - - - - - } - @if (Model.BillingAddress.StateProvinceEnabled) - { - - - - - } - @if (Model.BillingAddress.ZipPostalCodeEnabled) - { - - - - - } - @if (Model.BillingAddress.CountryEnabled) - { - - - - - } - @if (!string.IsNullOrEmpty(Model.BillingAddress.FormattedCustomAddressAttributes)) - { - - - - } - - - - - -
- @Loc["Admin.Orders.Address.FullName"]: - - @Model.BillingAddress.FirstName @Model.BillingAddress.LastName -
- @Loc["Admin.Orders.Address.Email"]: - - @Model.BillingAddress.Email -
- @Loc["Admin.Orders.Address.Phone"]: - - @Model.BillingAddress.PhoneNumber -
- @Loc["Admin.Orders.Address.Fax"]: - - @Model.BillingAddress.FaxNumber -
- @Loc["Admin.Orders.Address.Company"]: - - @Model.BillingAddress.Company -
- @Loc["Admin.Orders.Address.VatNumber"]: - - @Model.BillingAddress.VatNumber -
- @Loc["Admin.Orders.Address.Address1"]: - - @Model.BillingAddress.Address1 -
- @Loc["Admin.Orders.Address.Address2"]: - - @Model.BillingAddress.Address2 -
- @Loc["Admin.Orders.Address.City"]: - - @Model.BillingAddress.City -
- @Loc["Admin.Orders.Address.StateProvince"]: - - @Model.BillingAddress.StateProvinceName -
- @Loc["Admin.Orders.Address.ZipPostalCode"]: - - @Model.BillingAddress.ZipPostalCode -
- @Loc["Admin.Orders.Address.Country"]: - - @Model.BillingAddress.CountryName -
- @Html.Raw(Model.BillingAddress.FormattedCustomAddressAttributes) -
-  @Loc["Admin.Common.Edit"] -
-

-
-
- -@if (Model.IsShippable) -{ -
-
-

@Loc["Admin.Orders.ShippingInfo"]

-

- - @if (Model.ShippingAddress != null) - { - - @if (Model.ShippingAddress.FirstNameEnabled || Model.ShippingAddress.LastNameEnabled) - { - - - - - } - @if (Model.ShippingAddress.EmailEnabled) - { - - - - - } - @if (Model.ShippingAddress.PhoneEnabled) - { - - - - - } - @if (Model.ShippingAddress.FaxEnabled) - { - - - - - } - @if (Model.ShippingAddress.CompanyEnabled) - { - - - - - } - @if (Model.ShippingAddress.StreetAddressEnabled) - { - - - - - } - @if (Model.ShippingAddress.StreetAddress2Enabled) - { - - - - - } - @if (Model.ShippingAddress.CityEnabled) - { - - - - - } - @if (Model.ShippingAddress.StateProvinceEnabled) - { - - - - - } - @if (Model.ShippingAddress.ZipPostalCodeEnabled) - { - - - - - } - @if (Model.ShippingAddress.CountryEnabled) - { - - - - - } - @if (!string.IsNullOrEmpty(Model.ShippingAddress.FormattedCustomAddressAttributes)) - { - - - - } - - - - - -
- @Loc["Admin.Orders.Address.FullName"]: - - @Model.ShippingAddress.FirstName @Model.ShippingAddress.LastName -
- @Loc["Admin.Orders.Address.Email"]: - - @Model.ShippingAddress.Email -
- @Loc["Admin.Orders.Address.Phone"]: - - @Model.ShippingAddress.PhoneNumber -
- @Loc["Admin.Orders.Address.Fax"]: - - @Model.ShippingAddress.FaxNumber -
- @Loc["Admin.Orders.Address.Company"]: - - @Model.ShippingAddress.Company -
- @Loc["Admin.Orders.Address.Address1"]: - - @Model.ShippingAddress.Address1 -
- @Loc["Admin.Orders.Address.Address2"]: - - @Model.ShippingAddress.Address2 -
- @Loc["Admin.Orders.Address.City"]: - - @Model.ShippingAddress.City -
- @Loc["Admin.Orders.Address.StateProvince"]: - - @Model.ShippingAddress.StateProvinceName -
- @Loc["Admin.Orders.Address.ZipPostalCode"]: - - @Model.ShippingAddress.ZipPostalCode -
- @Loc["Admin.Orders.Address.Country"]: - - @Model.ShippingAddress.CountryName -
- @Html.Raw(Model.ShippingAddress.FormattedCustomAddressAttributes) -
-  @Loc["Admin.Common.Edit"] -
-

- } - @if (Model.PickupAddress != null) - { -
- -
- - @if (!string.IsNullOrEmpty(Model.PickupAddress.Address1)) - { - - - - - } - @if (!string.IsNullOrEmpty(Model.PickupAddress.City)) - { - - - - - } - @if (!string.IsNullOrEmpty(Model.PickupAddress.ZipPostalCode)) - { - - - - - } - @if (!string.IsNullOrEmpty(Model.PickupAddress.CountryName)) - { - - - - - } -
- @Loc["Admin.Orders.Address.Address1"]: - - @Model.PickupAddress.Address1 -
- @Loc["Admin.Orders.Address.City"]: - - @Model.PickupAddress.City -
- @Loc["Admin.Orders.Address.ZipPostalCode"]: - - @Model.PickupAddress.ZipPostalCode -
- @Loc["Admin.Orders.Address.Country"]: - - @Model.PickupAddress.CountryName -
-
-
- } - - @if (Model.IsShippable) - { -
-
- -
- -
- - - -
- -
- - - - - @if (!string.IsNullOrEmpty(Model.ShippingAdditionDescription)) - { -
- - } -
-
- -
- -
- -
-
- -
-
- } - -

-
-
-} -else -{ -
-
@Loc["Admin.Orders.ShippingInfo.NotRequired"]
-
-} - - \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/OrderDetails.Documents.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/OrderDetails.Documents.cshtml deleted file mode 100644 index 2a754f08e..000000000 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/OrderDetails.Documents.cshtml +++ /dev/null @@ -1,71 +0,0 @@ -@model OrderModel -@inject AdminAreaSettings adminAreaSettings -
- -
-
-
- - -
- - \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/OrderDetails.Info.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/OrderDetails.Info.cshtml deleted file mode 100644 index 9192a5900..000000000 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/OrderDetails.Info.cshtml +++ /dev/null @@ -1,545 +0,0 @@ -@using System.Text.Encodings.Web -@using Grand.Business.Core.Interfaces.Checkout.Orders -@using Grand.Domain.Payments -@using Grand.Domain.Tax -@using Constants = Grand.SharedUIResources.Constants -@model OrderModel -@inject IOrderTagService orderTagService - - - - - - -@{ - //tags - var orderTags = await orderTagService.GetAllOrderTags(); - var orderTagsSB = new StringBuilder(); - orderTagsSB.Append("var initialOrderTags = ["); - for (var i = 0; i < orderTags.Count; i++) - { - var tag = orderTags[i]; - orderTagsSB.Append("'"); - orderTagsSB.Append(JavaScriptEncoder.Default.Encode(tag.Name)); - orderTagsSB.Append("'"); - if (i != orderTags.Count - 1) - { - orderTagsSB.Append(","); - } - } - - orderTagsSB.Append("]"); -} - - - -
- -
-
- -
- -
-
- -
- -
-
-
- @Model.OrderStatus -
     - @if (Model.CanCancelOrder) - { - - } - -
-
-
@Loc["Admin.Orders.Fields.OrderStatus.Change.ForAdvancedUsers"]
- -
- - -
-
-
-
- -
- -
- -
-
- -
- -
- @{ - var labelStatus = ""; - switch (Model.PaymentStatusEnum) - { - case PaymentStatus.Paid: - labelStatus = "success"; - break; - case PaymentStatus.PartiallyPaid: - labelStatus = "info"; - break; - case PaymentStatus.Pending: - labelStatus = "default"; - break; - case PaymentStatus.Authorized: - labelStatus = "info"; - break; - case PaymentStatus.PartiallyRefunded: - labelStatus = "info"; - break; - case PaymentStatus.Refunded: - labelStatus = "warning"; - break; - case PaymentStatus.Voided: - labelStatus = "danger"; - break; - } - } - -
-
- -
- -
- -
- - -
-
-
- -@if (!string.IsNullOrEmpty(Model.Code)) -{ -
- -
- -
-
-} -
- -
- -
-
- -
- -
- -
-
- -
- -
- -
-
- -
- -
- -
-
-
- -
- -
-
-@if (!string.IsNullOrEmpty(Model.VatNumber)) -{ -
- -
- -
-
-} -@if (!string.IsNullOrEmpty(Model.AffiliateId)) -{ - -} -@if (!string.IsNullOrEmpty(Model.SalesEmployeeId)) -{ -
- -
- -
-
-} -@if (Model.TaxDisplayType == TaxDisplayType.IncludingTax) -{ -
- -
- -
-
-} -@if (Model.TaxDisplayType == TaxDisplayType.ExcludingTax) -{ -
- -
- -
-
-} -@if (Model.TaxDisplayType == TaxDisplayType.IncludingTax - && !string.IsNullOrEmpty(Model.OrderSubTotalDiscountInclTax)) -{ -
- -
- -
-
-} -@if (Model.TaxDisplayType == TaxDisplayType.ExcludingTax - && !string.IsNullOrEmpty(Model.OrderSubTotalDiscountExclTax)) -{ -
- -
- -
-
-} -@if (Model.TaxDisplayType == TaxDisplayType.IncludingTax) -{ -
- -
- -
-
-} -@if (Model.TaxDisplayType == TaxDisplayType.ExcludingTax) -{ -
- -
- -
-
-} -@if (Model.TaxDisplayType == TaxDisplayType.IncludingTax - && !string.IsNullOrEmpty(Model.PaymentMethodAdditionalFeeInclTax)) -{ -
- -
- -
-
-} -@if (Model.TaxDisplayType == TaxDisplayType.ExcludingTax - && !string.IsNullOrEmpty(Model.PaymentMethodAdditionalFeeExclTax)) -{ -
- -
- -
-
-} -@if (Model.DisplayTaxRates) -{ - foreach (var tr in Model.TaxRates) - { -
- -
- -
-
- } -} -@if (Model.DisplayTax) -{ -
- -
- -
-
-} -@if (!string.IsNullOrEmpty(Model.OrderTotalDiscount)) -{ -
- -
- -
-
-} -@foreach (var gc in Model.GiftVouchers) -{ -
- -
- - -
-
-} -@if (Model.RedeemedLoyaltyPoints > 0) -{ -
- -
- -
-
-} -
- -
- -
-
-@if (!string.IsNullOrEmpty(Model.RefundedAmount)) -{ -
- -
- -
-
-} -@if (!string.IsNullOrEmpty(Model.SuggestedRefundedAmount)) -{ -
- -
- -
-
-} -@if (Model.UsedDiscounts.Count > 0) -{ -
- -
- @for (var i = 0; i < Model.UsedDiscounts.Count; i++) - { - var discount = Model.UsedDiscounts[i]; - - } -
-
-} -
- -
- -
-
- -
- -
-
-
-
- -
- - -
-
- - -
-
-
- -
- - -
-
- - -
-
-
- -
- - -
-
- - -
-
-
- -
- - -
-
- - -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
-
-
-
-
- -
- - - -
-
-
- -
- -
-
- -@if (Model.CustomValues is { Count: > 0 }) -{ -
- -
- - @foreach (var item in Model.CustomValues) - { - - - - - } -
@item.Key:@(item.Value != null ? item.Value.ToString() : "")
-
-
-} -
- -
- -
-
-
- -
\ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/OrderDetails.Notifications.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/OrderDetails.Notifications.cshtml deleted file mode 100644 index 26fd22f38..000000000 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/OrderDetails.Notifications.cshtml +++ /dev/null @@ -1,72 +0,0 @@ -@model OrderModel -@inject AdminAreaSettings adminAreaSettings -
- -
-
-
- -
- - \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/OrderDetails.Shipment.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/OrderDetails.Shipment.cshtml deleted file mode 100644 index 1bac31908..000000000 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/OrderDetails.Shipment.cshtml +++ /dev/null @@ -1,167 +0,0 @@ -@using Grand.Business.Core.Interfaces.Common.Security -@using Grand.Domain.Permissions -@model OrderModel -@inject IPermissionService permissionService; - -@if (Model.IsShippable) -{ - @if (await permissionService.Authorize(PermissionSystemName.Shipments)) - { -
-
-

@Loc["Admin.Orders.Shipments"]

-
-
- -
-
-
- - -
-
-
-
- - - - } -} -else -{ -
-
@Loc["Admin.Orders.ShippingInfo.NotRequired"]
-
-} - - \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/ProductAddAttributes.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/ProductAddAttributes.cshtml deleted file mode 100644 index 31b110e06..000000000 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/ProductAddAttributes.cshtml +++ /dev/null @@ -1,96 +0,0 @@ -@model IList -@if (Model.Count > 0) -{ - foreach (var attribute in Model) - { -
- @{ - var controlId = $"attributes[{attribute.Id}]"; - var textPrompt = !string.IsNullOrEmpty(attribute.TextPrompt) ? attribute.TextPrompt : attribute.Name; - } - -
- @switch (attribute.AttributeControlType) - { - case AttributeControlType.DropdownList: - { - - } - break; - case AttributeControlType.RadioList: - case AttributeControlType.ColorSquares: - case AttributeControlType.ImageSquares: - { -
- @foreach (var attributeValue in attribute.Values) - { -
- - -
- } -
- } - break; - case AttributeControlType.Checkboxes: - case AttributeControlType.ReadonlyCheckboxes: - { -
- @foreach (var attributeValue in attribute.Values) - { -
- - -
- } -
- } - break; - case AttributeControlType.TextBox: - { - - } - break; - case AttributeControlType.MultilineTextbox: - { - - } - break; - case AttributeControlType.Datepicker: - { - - } - break; - case AttributeControlType.FileUpload: - { - - } - break; - } -
-
- } -} \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/ProductAddGiftVoucherInfo.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/ProductAddGiftVoucherInfo.cshtml deleted file mode 100644 index db636bd0e..000000000 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/ProductAddGiftVoucherInfo.cshtml +++ /dev/null @@ -1,42 +0,0 @@ -@model OrderModel.AddOrderProductModel.GiftVoucherModel -@if (Model.IsGiftVoucher) -{ -
-
- -
- -
-
-
- @if (Model.GiftVoucherType == GiftVoucherType.Virtual) - { - -
- -
- } -
-
- -
- -
-
-
- @if (Model.GiftVoucherType == GiftVoucherType.Virtual) - { - -
- -
- } -
-
- -
- -
-
-
-} \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.AddProducts.Bottom.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.AddProducts.Bottom.cshtml new file mode 100644 index 000000000..4d5f76158 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.AddProducts.Bottom.cshtml @@ -0,0 +1,2 @@ +@model OrderModel.AddOrderProductModel + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.AddProducts.Top.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.AddProducts.Top.cshtml new file mode 100644 index 000000000..82e5ac317 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.AddProducts.Top.cshtml @@ -0,0 +1,2 @@ +@model OrderModel.AddOrderProductModel + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.AddProductsDetails.Bottom.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.AddProductsDetails.Bottom.cshtml new file mode 100644 index 000000000..24dd6ce66 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.AddProductsDetails.Bottom.cshtml @@ -0,0 +1,2 @@ +@model OrderModel.AddOrderProductModel.ProductDetailsModel + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.AddProductsDetails.Top.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.AddProductsDetails.Top.cshtml new file mode 100644 index 000000000..a586feffa --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.AddProductsDetails.Top.cshtml @@ -0,0 +1,2 @@ +@model OrderModel.AddOrderProductModel.ProductDetailsModel + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.AddressDetails.Bottom.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.AddressDetails.Bottom.cshtml new file mode 100644 index 000000000..bd14f09f3 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.AddressDetails.Bottom.cshtml @@ -0,0 +1,2 @@ +@model OrderAddressModel + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.AddressDetails.Top.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.AddressDetails.Top.cshtml new file mode 100644 index 000000000..5f80fa923 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.AddressDetails.Top.cshtml @@ -0,0 +1,2 @@ +@model OrderAddressModel + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.AddressEditButtons.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.AddressEditButtons.cshtml new file mode 100644 index 000000000..eb624696d --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.AddressEditButtons.cshtml @@ -0,0 +1,2 @@ +@model OrderAddressModel + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Addresses.Bottom.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Addresses.Bottom.cshtml new file mode 100644 index 000000000..08e8abd76 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Addresses.Bottom.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Addresses.Top.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Addresses.Top.cshtml new file mode 100644 index 000000000..53f2aed1b --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Addresses.Top.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.DetailsButtons.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.DetailsButtons.cshtml new file mode 100644 index 000000000..df116bb01 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.DetailsButtons.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.DetailsTabs.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.DetailsTabs.cshtml new file mode 100644 index 000000000..df659cf79 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.DetailsTabs.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Documents.Bottom.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Documents.Bottom.cshtml new file mode 100644 index 000000000..3e02ab54f --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Documents.Bottom.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Documents.Top.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Documents.Top.cshtml new file mode 100644 index 000000000..8c4e3158e --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Documents.Top.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Info.Bottom.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Info.Bottom.cshtml new file mode 100644 index 000000000..9eb9e8b7e --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Info.Bottom.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Info.Top.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Info.Top.cshtml new file mode 100644 index 000000000..fdadd967d --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Info.Top.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Notes.Bottom.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Notes.Bottom.cshtml new file mode 100644 index 000000000..3ced33843 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Notes.Bottom.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Notes.Top.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Notes.Top.cshtml new file mode 100644 index 000000000..35ef6eca5 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Notes.Top.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Notifications.Bottom.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Notifications.Bottom.cshtml new file mode 100644 index 000000000..15d6dd706 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Notifications.Bottom.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Notifications.Top.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Notifications.Top.cshtml new file mode 100644 index 000000000..3ad6c8a1e --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Notifications.Top.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Products.Bottom.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Products.Bottom.cshtml new file mode 100644 index 000000000..cb94524e8 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Products.Bottom.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Products.Top.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Products.Top.cshtml new file mode 100644 index 000000000..add5b98ac --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Products.Top.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Shipment.AddressBottom.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Shipment.AddressBottom.cshtml new file mode 100644 index 000000000..08e8abd76 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Shipment.AddressBottom.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Shipment.AddressTop.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Shipment.AddressTop.cshtml new file mode 100644 index 000000000..53f2aed1b --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Shipment.AddressTop.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Shipment.ShippingBottom.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Shipment.ShippingBottom.cshtml new file mode 100644 index 000000000..33abf95d6 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Shipment.ShippingBottom.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Shipment.ShippingTop.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Shipment.ShippingTop.cshtml new file mode 100644 index 000000000..7bc4ef2ac --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.Shipment.ShippingTop.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.UploadLicenseButtons.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.UploadLicenseButtons.cshtml new file mode 100644 index 000000000..45dff1d9a --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Order/Partials/WidgetZone.UploadLicenseButtons.cshtml @@ -0,0 +1,2 @@ +@model OrderModel.UploadLicenseModel + diff --git a/src/Web/Grand.Web.Store/Controllers/OrderController.cs b/src/Web/Grand.Web.Store/Controllers/OrderController.cs index fe9ac87d6..cfaf40088 100644 --- a/src/Web/Grand.Web.Store/Controllers/OrderController.cs +++ b/src/Web/Grand.Web.Store/Controllers/OrderController.cs @@ -1,25 +1,26 @@ -using Grand.Business.Core.Commands.Checkout.Orders; -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.Localization; using Grand.Business.Core.Interfaces.Common.Pdf; -using Grand.Domain.Catalog; -using Grand.Domain.Common; using Grand.Domain.Orders; -using Grand.Domain.Permissions; using Grand.Infrastructure; -using Grand.Web.AdminShared.Extensions; -using Grand.Web.AdminShared.Interfaces; -using Grand.Web.AdminShared.Models.Orders; -using Grand.Web.Common.DataSource; -using Grand.Web.Common.Security.Authorization; using Grand.Mediator; +using Grand.Web.AdminShared.Controllers; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.Common.Filters; +using Grand.Web.Store.Extensions; using Microsoft.AspNetCore.Mvc; namespace Grand.Web.Store.Controllers; -[PermissionAuthorize(PermissionSystemName.Orders)] +// Concrete host subclass of BaseOrderManagementController (ARCH-001 Order consolidation). This class +// supplies Store's DI wiring plus the attributes that used to arrive transitively via +// BaseStoreController - BaseOrderManagementController can't inherit any single host's base controller +// (it's shared across Admin/Store, each with a different [Area]/[Authorize*] pair), so each subclass +// restates its own host's attribute set explicitly, same pattern as CategoryController/ProductController. +[AutoValidateAntiforgeryToken] +[Area(Constants.AreaStore)] +[AuthorizeStore] +[AuthorizeMenu] public class OrderController( IOrderViewModelService orderViewModelService, IOrderService orderService, @@ -27,832 +28,7 @@ public class OrderController( ITranslationService translationService, IContextAccessor contextAccessor, IPdfService pdfService, - IMediator mediator) : BaseStoreController -{ - - #region Order list - - public IActionResult Index() - { - return RedirectToAction("List"); - } - - public async Task List(int? orderStatusId = null, - int? paymentStatusId = null, int? shippingStatusId = null, DateTime? startDate = null, string code = null) - { - var model = await orderViewModelService.PrepareOrderListModel(orderStatusId, paymentStatusId, shippingStatusId, - startDate, contextAccessor.WorkContext.CurrentCustomer.StaffStoreId, code); - return View(model); - } - - public async Task ProductSearchAutoComplete(string term, - [FromServices] IProductService productService) - { - const int searchTermMinimumLength = 3; - if (string.IsNullOrWhiteSpace(term) || term.Length < searchTermMinimumLength) - return Content(""); - - //products - const int productNumber = 15; - var products = (await productService.SearchProducts( - storeId: contextAccessor.WorkContext.CurrentCustomer.StaffStoreId, - keywords: term, - pageSize: productNumber, - showHidden: true)).products; - - var result = (from p in products - select new - { - label = p.Name, - productid = p.Id - }) - .ToList(); - return Json(result); - } - - [PermissionAuthorizeAction(PermissionActionName.List)] - [HttpPost] - public async Task OrderList(DataSourceRequest command, OrderListModel model) - { - model.StoreId = contextAccessor.WorkContext.CurrentCustomer.StaffStoreId; - - var (orderModels, totalCount) = - await orderViewModelService.PrepareOrderModel(model, command.Page, command.PageSize); - - var gridModel = new DataSourceResult { - Data = orderModels.ToList(), - Total = totalCount - }; - return Json(gridModel); - } - - [PermissionAuthorizeAction(PermissionActionName.Preview)] - [HttpPost] - public async Task GoToOrderId(OrderListModel model) - { - Order order = null; - int.TryParse(model.GoDirectlyToNumber, out var orderNumber); - if (orderNumber > 0) order = await orderService.GetOrderByNumber(orderNumber); - var orders = await orderService.GetOrdersByCode(model.GoDirectlyToNumber); - switch (orders.Count) - { - case > 1: - return RedirectToAction("List", new { Code = model.GoDirectlyToNumber }); - case 1: - order = orders.FirstOrDefault(); - break; - case 0: - return RedirectToAction("List", new { Code = model.GoDirectlyToNumber }); - } - - if (order!.StoreId != contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) - return RedirectToAction("List"); - - return RedirectToAction("Edit", "Order", new { id = order.Id }); - } - - #endregion - - #region Order details - - #region Payments and other order workflow - - [PermissionAuthorizeAction(PermissionActionName.Cancel)] - [HttpGet] - public async Task CancelOrder(string id) - { - var order = await orderService.GetOrderById(id); - if (order == null) - //No order found with the specified id - return RedirectToAction("List"); - - if (order.StoreId != contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) return RedirectToAction("List"); - - try - { - await mediator.Send(new CancelOrderCommand { Order = order, NotifyCustomer = true }); - - Success("Successfully canceled order"); - return RedirectToAction("Edit", "Order", new { id }); - } - catch (Exception exc) - { - //error - Error(exc); - return RedirectToAction("Edit", "Order", new { id }); - } - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - [HttpPost] - public async Task SaveOrderTags(OrderModel orderModel) - { - var order = await orderService.GetOrderById(orderModel.Id); - if (order == null) - //No order found with the specified id - return RedirectToAction("List"); - - if (order.StoreId != contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) return RedirectToAction("List"); - - try - { - await orderViewModelService.SaveOrderTags(order, orderModel.OrderTags); - - var model = new OrderModel(); - await orderViewModelService.PrepareOrderDetailsModel(model, order); - return RedirectToAction("Edit", "Order", new { id = order.Id }); - } - catch (Exception exception) - { - //error - Error(exception, false); - return RedirectToAction("Edit", "Order", new { id = order.Id }); - } - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - [HttpPost] - public async Task ChangeOrderStatus(string id, OrderModel model) - { - var order = await orderService.GetOrderById(id); - if (order == null) - //No order found with the specified id - return RedirectToAction("List"); - - if (order.StoreId != contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) return RedirectToAction("List"); - - try - { - var status = await orderStatusService.GetByStatusId(model.OrderStatusId); - ArgumentNullException.ThrowIfNull(status); - - order.OrderStatusId = model.OrderStatusId; - await orderService.UpdateOrder(order); - - //add a note - await orderService.InsertOrderNote(new OrderNote { - Note = $"Order status has been edited. New status: {status.Name}", - DisplayToCustomer = false, - OrderId = order.Id - }); - model = new OrderModel(); - await orderViewModelService.PrepareOrderDetailsModel(model, order); - return RedirectToAction("Edit", "Order", new { id }); - } - catch (Exception exc) - { - //error - Error(exc, false); - return RedirectToAction("Edit", "Order", new { id }); - } - } - - #endregion - - #region Edit, delete - - [PermissionAuthorizeAction(PermissionActionName.Preview)] - public async Task Edit(string id) - { - var order = await orderService.GetOrderById(id); - if (order == null || order.Deleted) - //No order found with the specified id - return RedirectToAction("List"); - - if (order.StoreId != contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) return RedirectToAction("List"); - - var model = new OrderModel(); - await orderViewModelService.PrepareOrderDetailsModel(model, order); - - return View(model); - } - - [PermissionAuthorizeAction(PermissionActionName.Delete)] - [HttpPost] - public async Task Delete(OrderDeleteModel model) - { - var order = await orderService.GetOrderById(model.Id); - if (order == null) - //No order found with the specified id - return RedirectToAction("List"); - - if (order.StoreId != contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) return RedirectToAction("List"); - - if (ModelState.IsValid) - { - await mediator.Send(new DeleteOrderCommand { Order = order }); - - return RedirectToAction("List"); - } - - Error(ModelState); - return RedirectToAction("Edit", "Order", new { model.Id }); - } - - public async Task PdfInvoice(string orderId) - { - var order = await orderService.GetOrderById(orderId); - if (order.StoreId != contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) return RedirectToAction("List"); - - var orders = new List { - order - }; - byte[] bytes; - using (var stream = new MemoryStream()) - { - await pdfService.PrintOrdersToPdf(stream, orders, contextAccessor.WorkContext.WorkingLanguage.Id); - bytes = stream.ToArray(); - } - - return File(bytes, "application/pdf", $"order_{order.Id}.pdf"); - } - - [PermissionAuthorizeAction(PermissionActionName.Export)] - [HttpPost] - public async Task PdfInvoiceAll(OrderListModel model) - { - //load orders - var orders = await orderViewModelService.PrepareOrders(model); - orders = orders.Where(x => x.StoreId == contextAccessor.WorkContext.CurrentCustomer.StaffStoreId).ToList(); - - byte[] bytes; - using (var stream = new MemoryStream()) - { - await pdfService.PrintOrdersToPdf(stream, orders, contextAccessor.WorkContext.WorkingLanguage.Id, model.VendorId); - bytes = stream.ToArray(); - } - - return File(bytes, "application/pdf", "orders.pdf"); - } - - [PermissionAuthorizeAction(PermissionActionName.Export)] - [HttpPost] - public async Task PdfInvoiceSelected(string selectedIds) - { - var orders = new List(); - if (selectedIds != null) - { - var ids = selectedIds - .Split([','], StringSplitOptions.RemoveEmptyEntries) - .Select(x => x) - .ToArray(); - orders.AddRange(await orderService.GetOrdersByIds(ids)); - } - - orders = orders.Where(x => x.StoreId == contextAccessor.WorkContext.CurrentCustomer.StaffStoreId).ToList(); - - //ensure that we at least one order selected - if (orders.Count == 0) - { - Error(translationService.GetResource("Admin.Orders.PdfInvoice.NoOrders")); - return RedirectToAction("List"); - } - - byte[] bytes; - using (var stream = new MemoryStream()) - { - await pdfService.PrintOrdersToPdf(stream, orders, contextAccessor.WorkContext.WorkingLanguage.Id); - bytes = stream.ToArray(); - } - - return File(bytes, "application/pdf", "orders.pdf"); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - [HttpPost] - public async Task EditOrderTotals(string id, OrderModel model) - { - var order = await orderService.GetOrderById(id); - if (order == null) - //No order found with the specified id - return RedirectToAction("List"); - - if (order.StoreId != contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) - return RedirectToAction("List"); - - order.OrderSubtotalInclTax = model.OrderSubtotalInclTaxValue; - order.OrderSubtotalExclTax = model.OrderSubtotalExclTaxValue; - order.OrderSubTotalDiscountInclTax = model.OrderSubTotalDiscountInclTaxValue; - order.OrderSubTotalDiscountExclTax = model.OrderSubTotalDiscountExclTaxValue; - order.OrderShippingInclTax = model.OrderShippingInclTaxValue; - order.OrderShippingExclTax = model.OrderShippingExclTaxValue; - order.PaymentMethodAdditionalFeeInclTax = model.PaymentMethodAdditionalFeeInclTaxValue; - order.PaymentMethodAdditionalFeeExclTax = model.PaymentMethodAdditionalFeeExclTaxValue; - order.OrderTax = model.TaxValue; - order.OrderDiscount = model.OrderTotalDiscountValue; - order.OrderTotal = model.OrderTotalValue; - order.CurrencyRate = model.CurrencyRate; - await orderService.UpdateOrder(order); - - //add a note - await orderService.InsertOrderNote(new OrderNote { - Note = "Order totals have been edited", - DisplayToCustomer = false, - OrderId = order.Id - }); - - await orderViewModelService.PrepareOrderDetailsModel(model, order); - return RedirectToAction("Edit", "Order", new { id }); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - [HttpPost] - public async Task EditShippingMethod(string id, OrderModel model) - { - var order = await orderService.GetOrderById(id); - if (order == null) - //No order found with the specified id - return RedirectToAction("List"); - - if (order.StoreId != contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) - return RedirectToAction("List"); - - order.ShippingMethod = model.ShippingMethod; - await orderService.UpdateOrder(order); - - //add a note - await orderService.InsertOrderNote(new OrderNote { - Note = "Shipping method has been edited", - DisplayToCustomer = false, - OrderId = order.Id - }); - await orderViewModelService.PrepareOrderDetailsModel(model, order); - - //selected tab - await SaveSelectedTabIndex(persistForTheNextRequest: true); - - return RedirectToAction("Edit", "Order", new { id }); - } - - [HttpPost] - public async Task EditUserFields(string id, OrderModel model) - { - var order = await orderService.GetOrderById(id); - if (order == null) - //No order found with the specified id - return RedirectToAction("List"); - - if (order.StoreId != contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) - return RedirectToAction("List"); - - order.UserFields = model.UserFields; - - await orderService.UpdateOrder(order); - - await orderViewModelService.PrepareOrderDetailsModel(model, order); - - //selected tab - await SaveSelectedTabIndex(persistForTheNextRequest: true); - - return RedirectToAction("Edit", "Order", new { id }); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - [HttpPost] - public async Task SaveOrderItem(string id, OrderItemsModel model) - { - var order = await orderService.GetOrderById(id); - if (order == null) - //No order found with the specified id - return RedirectToAction("List"); - - if (order.StoreId != contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) - return RedirectToAction("List"); - - if (order.OrderStatusId == (int)OrderStatusSystem.Cancelled) - { - Error("You can't edit position when order is canceled"); - return RedirectToAction("Edit", "Order", new { id }); - } - - var orderItem = order.OrderItems.FirstOrDefault(x => x.Id == model.OrderItemId) ?? throw new ArgumentException("No order item found with the specified id"); - var itemModel = model.Items.FirstOrDefault(x => x.Id == model.OrderItemId) ?? throw new ArgumentException("No order item model found with the specified id"); - - if (itemModel.Quantity == 0 || (orderItem.OpenQty != orderItem.Quantity && orderItem.IsShipEnabled)) - { - Error("You can't change quantity"); - return RedirectToAction("Edit", "Order", new { id }); - } - - if (orderItem.Quantity == itemModel.Quantity && orderItem.UnitPriceExclTax == itemModel.UnitPriceExclTaxValue) - { - Error("Nothing has been changed"); - return RedirectToAction("Edit", "Order", new { id }); - } - - orderItem.Quantity = itemModel.Quantity; - orderItem.OpenQty = itemModel.Quantity; - - if (orderItem.UnitPriceExclTax != itemModel.UnitPriceExclTaxValue) - { - orderItem.UnitPriceExclTax = itemModel.UnitPriceExclTaxValue; - orderItem.UnitPriceInclTax = - Math.Round(orderItem.UnitPriceExclTax * orderItem.TaxRate / 100 + orderItem.UnitPriceExclTax, 2); - orderItem.PriceInclTax = Math.Round(orderItem.UnitPriceInclTax * orderItem.Quantity, 2); - orderItem.PriceExclTax = Math.Round(orderItem.UnitPriceExclTax * orderItem.Quantity, 2); - - orderItem.DiscountAmountInclTax = 0; - orderItem.DiscountAmountExclTax = 0; - } - else - { - orderItem.PriceInclTax = Math.Round(orderItem.UnitPriceInclTax * orderItem.Quantity, 2); - orderItem.PriceExclTax = Math.Round(orderItem.UnitPriceExclTax * orderItem.Quantity, 2); - - orderItem.DiscountAmountInclTax = 0; - orderItem.DiscountAmountExclTax = 0; - } - - await mediator.Send(new UpdateOrderItemCommand { Order = order, OrderItem = orderItem }); - - //selected tab - await SaveSelectedTabIndex(persistForTheNextRequest: true); - - return RedirectToAction("Edit", "Order", new { id }); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - [HttpPost] - public async Task DeleteOrderItem(string id, string orderItemId) - { - var order = await orderService.GetOrderById(id); - if (order == null) - //No order found with the specified id - return RedirectToAction("List"); - - if (order.StoreId != contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) - return RedirectToAction("List"); - - var orderItem = order.OrderItems.FirstOrDefault(x => x.Id == orderItemId) ?? throw new ArgumentException("No order item found with the specified id"); - var result = await mediator.Send(new DeleteOrderItemCommand { Order = order, OrderItem = orderItem }); - if (result.error) - Error(result.message); - - //selected tab - await SaveSelectedTabIndex(persistForTheNextRequest: true); - - return RedirectToAction("Edit", "Order", new { id }); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - [HttpPost] - public async Task CancelOrderItem(string id, string orderItemId) - { - var order = await orderService.GetOrderById(id); - if (order == null) - //No order found with the specified id - return RedirectToAction("List"); - - if (order.StoreId != contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) - return RedirectToAction("List"); - - var orderItem = order.OrderItems.FirstOrDefault(x => x.Id == orderItemId) ?? throw new ArgumentException("No order item found with the specified id"); - var result = await mediator.Send(new CancelOrderItemCommand { Order = order, OrderItem = orderItem }); - if (result.error) - Error(result.message); - else - Success("The order item was successfully canceled"); - - //selected tab - await SaveSelectedTabIndex(persistForTheNextRequest: true); - - return RedirectToAction("Edit", "Order", new { id }); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - [HttpPost] - public async Task ResetDownloadCount(string id, string orderItemId) - { - var order = await orderService.GetOrderById(id); - if (order == null) - //No order found with the specified id - return RedirectToAction("List"); - - if (order.StoreId != contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) - return RedirectToAction("List"); - - var orderItem = order.OrderItems.FirstOrDefault(x => x.Id == orderItemId) ?? throw new ArgumentException("No order item found with the specified id"); - orderItem.DownloadCount = 0; - await orderService.UpdateOrder(order); - var model = new OrderModel(); - await orderViewModelService.PrepareOrderDetailsModel(model, order); - - //selected tab - await SaveSelectedTabIndex(persistForTheNextRequest: true); - - return RedirectToAction("Edit", "Order", new { id }); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - [HttpPost] - public async Task ActivateDownloadItem(string id, string orderItemId) - { - var order = await orderService.GetOrderById(id); - if (order == null) - //No order found with the specified id - return RedirectToAction("List"); - - if (order.StoreId != contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) - return RedirectToAction("List"); - - var orderItem = order.OrderItems.FirstOrDefault(x => x.Id == orderItemId) ?? throw new ArgumentException("No order item found with the specified id"); - orderItem.IsDownloadActivated = !orderItem.IsDownloadActivated; - await orderService.UpdateOrder(order); - var model = new OrderModel(); - await orderViewModelService.PrepareOrderDetailsModel(model, order); - - //selected tab - await SaveSelectedTabIndex(persistForTheNextRequest: true); - - return RedirectToAction("Edit", "Order", new { id }); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - public async Task UploadLicenseFilePopup(string id, string orderItemId, - [FromServices] IProductService productService) - { - var order = await orderService.GetOrderById(id); - if (order == null) - //No order found with the specified id - return RedirectToAction("List"); - - if (order.StoreId != contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) - return RedirectToAction("List"); - - var orderItem = order.OrderItems.FirstOrDefault(x => x.Id == orderItemId) ?? throw new ArgumentException("No order item found with the specified id"); - var product = await productService.GetProductByIdIncludeArch(orderItem.ProductId); - - if (!product.IsDownload) - throw new ArgumentException("Product is not downloadable"); - var model = new OrderModel.UploadLicenseModel { - LicenseDownloadId = !string.IsNullOrEmpty(orderItem.LicenseDownloadId) ? orderItem.LicenseDownloadId : "", - OrderId = order.Id, - OrderItemId = orderItem.Id - }; - - return View(model); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - [HttpPost] - public async Task UploadLicenseFilePopup(OrderModel.UploadLicenseModel model) - { - var order = await orderService.GetOrderById(model.OrderId); - if (order == null) - //No order found with the specified id - return RedirectToAction("List"); - - if (order.StoreId != contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) - return RedirectToAction("List"); - - var orderItem = order.OrderItems.FirstOrDefault(x => x.Id == model.OrderItemId) ?? throw new ArgumentException("No order item found with the specified id"); - - //attach license - orderItem.LicenseDownloadId = !string.IsNullOrEmpty(model.LicenseDownloadId) ? model.LicenseDownloadId : null; - await orderService.UpdateOrder(order); - - //success - model.RefreshPage = true; - - return View(model); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - [HttpPost] - public async Task DeleteLicenseFilePopup(OrderModel.UploadLicenseModel model) - { - var order = await orderService.GetOrderById(model.OrderId); - if (order == null) - //No order found with the specified id - return RedirectToAction("List"); - - if (order.StoreId != contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) - return RedirectToAction("List"); - - var orderItem = order.OrderItems.FirstOrDefault(x => x.Id == model.OrderItemId); - if (orderItem == null) - throw new ArgumentException("No order item found with the specified id"); - - //attach license - orderItem.LicenseDownloadId = null; - await orderService.UpdateOrder(order); - - return RedirectToAction("Edit", "Order", new { id = model.OrderId }); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - public async Task AddProductToOrder(string orderId) - { - var order = await orderService.GetOrderById(orderId); - if (order == null) - //No order found with the specified id - return RedirectToAction("List"); - - if (order.StoreId != contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) - return RedirectToAction("List"); - - var model = await orderViewModelService.PrepareAddOrderProductModel(order); - return View(model); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - [HttpPost] - public async Task AddProductToOrder(DataSourceRequest command, OrderModel.AddOrderProductModel model, - [FromServices] IProductService productService) - { - var categoryIds = new List(); - if (!string.IsNullOrEmpty(model.SearchCategoryId)) - categoryIds.Add(model.SearchCategoryId); - - var gridModel = new DataSourceResult(); - var products = (await productService.SearchProducts(categoryIds: categoryIds, - storeId: contextAccessor.WorkContext.CurrentCustomer.StaffStoreId, - brandId: model.SearchBrandId, - collectionId: model.SearchCollectionId, - productType: model.SearchProductTypeId > 0 ? (ProductType?)model.SearchProductTypeId : null, - keywords: model.SearchProductName, - pageIndex: command.Page - 1, - pageSize: command.PageSize, - showHidden: true)).products; - gridModel.Data = products.Select(x => - { - var productModel = new OrderModel.AddOrderProductModel.ProductModel { - Id = x.Id, - Name = x.Name, - Sku = x.Sku - }; - - return productModel; - }); - gridModel.Total = products.TotalCount; - - return Json(gridModel); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - public async Task AddProductToOrderDetails(string orderId, string productId) - { - var order = await orderService.GetOrderById(orderId); - if (order == null) - return RedirectToAction("List"); - - if (order.StoreId != contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) return RedirectToAction("List"); - - var model = await orderViewModelService.PrepareAddProductToOrderModel(order, productId); - return View(model); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - [HttpPost] - public async Task AddProductToOrderDetails(AddProductToOrderModel model) - { - var order = await orderService.GetOrderById(model.OrderId); - if (order == null) - return RedirectToAction("List"); - - if (order.StoreId != contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) return RedirectToAction("List"); - - var warnings = await orderViewModelService.AddProductToOrderDetails(model); - if (!warnings.Any()) - //redirect to order details page - return RedirectToAction("Edit", "Order", new { id = model.OrderId }); - - //errors - var result = await orderViewModelService.PrepareAddProductToOrderModel(order, model.ProductId); - result.Warnings.AddRange(warnings); - return View(result); - } - - #endregion - - #endregion - - #region Addresses - - [PermissionAuthorizeAction(PermissionActionName.Preview)] - public async Task AddressEdit(string addressId, string orderId, bool billingAddress) - { - var order = await orderService.GetOrderById(orderId); - if (order == null) - //No order found with the specified id - return RedirectToAction("List"); - - if (order.StoreId != contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) return RedirectToAction("List"); - - var address = new Address(); - switch (billingAddress) - { - case true when order.BillingAddress != null: - { - if (order.BillingAddress.Id == addressId) - address = order.BillingAddress; - break; - } - case false when order.ShippingAddress != null: - { - if (order.ShippingAddress.Id == addressId) - address = order.ShippingAddress; - break; - } - } - - if (address == null) - throw new ArgumentException("No address found with the specified id", nameof(addressId)); - - var model = await orderViewModelService.PrepareOrderAddressModel(order, address); - model.BillingAddress = billingAddress; - return View(model); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - [HttpPost] - public async Task AddressEdit(OrderAddressModel model, - [FromServices] IAddressAttributeService addressAttributeService, - [FromServices] IAddressAttributeParser addressAttributeParser) - { - var order = await orderService.GetOrderById(model.OrderId); - if (order == null) - //No order found with the specified id - return RedirectToAction("List"); - - if (order.StoreId != contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) return RedirectToAction("List"); - - var address = new Address(); - switch (model.BillingAddress) - { - case true when order.BillingAddress != null: - { - if (order.BillingAddress.Id == model.Address.Id) - address = order.BillingAddress; - break; - } - case false when order.ShippingAddress != null: - { - if (order.ShippingAddress.Id == model.Address.Id) - address = order.ShippingAddress; - break; - } - } - - if (ModelState.IsValid) - { - var customAttributes = await model.Address.ParseCustomAddressAttributes(addressAttributeParser, addressAttributeService); - await orderViewModelService.UpdateOrderAddress(order, address, model, customAttributes); - return RedirectToAction("AddressEdit", - new { addressId = model.Address.Id, orderId = model.OrderId, model.BillingAddress }); - } - - //If we got this far, something failed, redisplay form - model = await orderViewModelService.PrepareOrderAddressModel(order, address); - return View(model); - } - - #endregion - - #region Order notes - - [PermissionAuthorizeAction(PermissionActionName.List)] - [HttpPost] - public async Task OrderNotesSelect(string orderId, DataSourceRequest command) - { - var order = await orderService.GetOrderById(orderId) ?? throw new ArgumentException("No order found with the specified id"); - if (order.StoreId != contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) return Content(""); - - //order notes - var orderNoteModels = await orderViewModelService.PrepareOrderNotes(order); - var gridModel = new DataSourceResult { - Data = orderNoteModels, - Total = orderNoteModels.Count - }; - return Json(gridModel); - } - - [PermissionAuthorizeAction(PermissionActionName.Edit)] - public async Task OrderNoteAdd(string orderId, string downloadId, bool displayToCustomer, - string message) - { - var order = await orderService.GetOrderById(orderId); - if (order == null) - return Json(new { Result = false }); - - if (order.StoreId != contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) return Json(new { Result = false }); - - await orderViewModelService.InsertOrderNote(order, downloadId, displayToCustomer, message); - - return Json(new { Result = true }); - } - - [PermissionAuthorizeAction(PermissionActionName.Delete)] - [HttpPost] - public async Task OrderNoteDelete(string id, string orderId) - { - var order = await orderService.GetOrderById(orderId) ?? throw new ArgumentException("No order found with the specified id"); - if (order.StoreId != contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) return Json(new { Result = false }); - - await orderViewModelService.DeleteOrderNote(order, id); - - return new JsonResult(""); - } - - #endregion -} \ No newline at end of file + IMediator mediator, + IAdminDataScope scope) + : BaseOrderManagementController(orderViewModelService, orderService, orderStatusService, + translationService, contextAccessor, pdfService, mediator, scope); diff --git a/src/Web/Grand.Web.Vendor/Areas/Vendor/Views/Order/Partials/WidgetZone.Shipment.AddressBottom.cshtml b/src/Web/Grand.Web.Vendor/Areas/Vendor/Views/Order/Partials/WidgetZone.Shipment.AddressBottom.cshtml new file mode 100644 index 000000000..94a9838b8 --- /dev/null +++ b/src/Web/Grand.Web.Vendor/Areas/Vendor/Views/Order/Partials/WidgetZone.Shipment.AddressBottom.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + diff --git a/src/Web/Grand.Web.Vendor/Areas/Vendor/Views/Order/Partials/WidgetZone.Shipment.AddressTop.cshtml b/src/Web/Grand.Web.Vendor/Areas/Vendor/Views/Order/Partials/WidgetZone.Shipment.AddressTop.cshtml new file mode 100644 index 000000000..3f0c2f351 --- /dev/null +++ b/src/Web/Grand.Web.Vendor/Areas/Vendor/Views/Order/Partials/WidgetZone.Shipment.AddressTop.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + diff --git a/src/Web/Grand.Web.Vendor/Areas/Vendor/Views/Order/Partials/WidgetZone.Shipment.ShippingBottom.cshtml b/src/Web/Grand.Web.Vendor/Areas/Vendor/Views/Order/Partials/WidgetZone.Shipment.ShippingBottom.cshtml new file mode 100644 index 000000000..f867c800b --- /dev/null +++ b/src/Web/Grand.Web.Vendor/Areas/Vendor/Views/Order/Partials/WidgetZone.Shipment.ShippingBottom.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + diff --git a/src/Web/Grand.Web.Vendor/Areas/Vendor/Views/Order/Partials/WidgetZone.Shipment.ShippingTop.cshtml b/src/Web/Grand.Web.Vendor/Areas/Vendor/Views/Order/Partials/WidgetZone.Shipment.ShippingTop.cshtml new file mode 100644 index 000000000..f819a23dd --- /dev/null +++ b/src/Web/Grand.Web.Vendor/Areas/Vendor/Views/Order/Partials/WidgetZone.Shipment.ShippingTop.cshtml @@ -0,0 +1,2 @@ +@model OrderModel + 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 83c4d41cb..7459fba57 100644 --- a/src/Web/Grand.Web.Vendor/Areas/Vendor/Views/_ViewImports.cshtml +++ b/src/Web/Grand.Web.Vendor/Areas/Vendor/Views/_ViewImports.cshtml @@ -30,7 +30,16 @@ 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; -@using Grand.Web.Vendor.Models.Orders; +@* Order views bind to Grand.Web.AdminShared's OrderModel/OrderListModel (ARCH-001 Phase 5 Order + consolidation) - Grand.Web.Vendor.Models.Orders's own OrderModel/OrderListModel are no longer + used by any controller/view. Unlike the ProductModel case above, this can't be a blanket + "@using Grand.Web.AdminShared.Models.Orders" - that namespace also holds ShipmentModel, + MerchandiseReturnModel, *ReportModel etc., which Vendor's Shipment/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 two types + that are actually consolidated. *@ +@using OrderModel = Grand.Web.AdminShared.Models.Orders.OrderModel; +@using OrderListModel = Grand.Web.AdminShared.Models.Orders.OrderListModel; @using Grand.Web.Vendor.Models.Shipment; @using Grand.Web.Vendor.Models.MerchandiseReturn; @using Grand.Web.Vendor.Models.Vendor; diff --git a/src/Web/Grand.Web.Vendor/Controllers/OrderController.cs b/src/Web/Grand.Web.Vendor/Controllers/OrderController.cs index 66a810779..13ed30012 100644 --- a/src/Web/Grand.Web.Vendor/Controllers/OrderController.cs +++ b/src/Web/Grand.Web.Vendor/Controllers/OrderController.cs @@ -1,221 +1,33 @@ -using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Business.Core.Interfaces.Checkout.Orders; using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Common.Pdf; -using Grand.Domain.Permissions; using Grand.Domain.Orders; using Grand.Infrastructure; -using Grand.Web.Common.DataSource; -using Grand.Web.Common.Security.Authorization; +using Grand.Web.AdminShared.Controllers; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.Common.Filters; using Grand.Web.Vendor.Extensions; -using Grand.Web.Vendor.Interfaces; -using Grand.Web.Vendor.Models.Orders; using Microsoft.AspNetCore.Mvc; namespace Grand.Web.Vendor.Controllers; -[PermissionAuthorize(PermissionSystemName.Orders)] -public class OrderController : BaseVendorController -{ - #region Ctor - - public OrderController( - IOrderViewModelService orderViewModelService, - IOrderService orderService, - ITranslationService translationService, - IContextAccessor contextAccessor, - IPdfService pdfService) - { - _orderViewModelService = orderViewModelService; - _orderService = orderService; - _translationService = translationService; - _contextAccessor = contextAccessor; - _pdfService = pdfService; - } - - #endregion - - #region Fields - - private readonly IOrderViewModelService _orderViewModelService; - private readonly IOrderService _orderService; - private readonly ITranslationService _translationService; - private readonly IContextAccessor _contextAccessor; - private readonly IPdfService _pdfService; - - #endregion - - #region Order list - - public IActionResult Index() - { - return RedirectToAction("List"); - } - - public async Task List(int? orderStatusId = null, - int? paymentStatusId = null, int? shippingStatusId = null, - DateTime? startDate = null, string code = null) - { - var model = await _orderViewModelService.PrepareOrderListModel(orderStatusId, paymentStatusId, shippingStatusId, - startDate, code); - return View(model); - } - - public async Task ProductSearchAutoComplete(string term, - [FromServices] IProductService productService) - { - const int searchTermMinimumLength = 3; - if (string.IsNullOrWhiteSpace(term) || term.Length < searchTermMinimumLength) - return Content(""); - - //products - const int productNumber = 15; - var products = (await productService.SearchProducts( - vendorId: _contextAccessor.WorkContext.CurrentVendor.Id, - keywords: term, - pageSize: productNumber, - showHidden: true)).products; - - var result = (from p in products - select new { - label = p.Name, - productid = p.Id - }) - .ToList(); - return Json(result); - } - - [PermissionAuthorizeAction(PermissionActionName.List)] - [HttpPost] - public async Task OrderList(DataSourceRequest command, OrderListModel model) - { - var (orderModels, totalCount) = - await _orderViewModelService.PrepareOrderModel(model, command.Page, command.PageSize); - var gridModel = new DataSourceResult { - Data = orderModels.ToList(), - Total = totalCount - }; - return Json(gridModel); - } - - [PermissionAuthorizeAction(PermissionActionName.Preview)] - [HttpPost] - public async Task GoToOrderId(OrderListModel model) - { - Order order = null; - if (int.TryParse(model.GoDirectlyToNumber, out var orderNumber)) - { - order = await _orderService.GetOrderByNumber(orderNumber); - } - else - { - var orders = await _orderService.GetOrdersByCode(model.GoDirectlyToNumber); - switch (orders.Count) - { - case > 1: - return RedirectToAction("List", new { Code = model.GoDirectlyToNumber }); - case 1: - order = orders.FirstOrDefault(); - break; - } - } - - return RedirectToAction("Edit", "Order", new { id = order?.Id }); - } - - #endregion - - #region Order details - - #region Edit, delete - - [PermissionAuthorizeAction(PermissionActionName.Preview)] - public async Task Edit(string id) - { - var order = await _orderService.GetOrderById(id); - if (order == null || order.Deleted || !_contextAccessor.WorkContext.HasAccessToOrder(order)) - //No order found with the specified id - return RedirectToAction("List"); - - var model = new OrderModel(); - await _orderViewModelService.PrepareOrderDetailsModel(model, order); - - return View(model); - } - - public async Task PdfInvoice(string orderId) - { - var order = await _orderService.GetOrderById(orderId); - //No order found with the specified id - if (order == null || order.Deleted || !_contextAccessor.WorkContext.HasAccessToOrder(order)) return RedirectToAction("List"); - - var orders = new List { - order - }; - byte[] bytes; - using (var stream = new MemoryStream()) - { - await _pdfService.PrintOrdersToPdf(stream, orders, _contextAccessor.WorkContext.WorkingLanguage.Id, - _contextAccessor.WorkContext.CurrentVendor.Id); - bytes = stream.ToArray(); - } - - return File(bytes, "application/pdf", $"order_{order.Id}.pdf"); - } - - [PermissionAuthorizeAction(PermissionActionName.Export)] - [HttpPost] - public async Task PdfInvoiceAll(OrderListModel model) - { - //load orders - var orders = await _orderViewModelService.PrepareOrders(model); - byte[] bytes; - using (var stream = new MemoryStream()) - { - await _pdfService.PrintOrdersToPdf(stream, orders, _contextAccessor.WorkContext.WorkingLanguage.Id, - _contextAccessor.WorkContext.CurrentVendor.Id); - bytes = stream.ToArray(); - } - - return File(bytes, "application/pdf", "orders.pdf"); - } - - [PermissionAuthorizeAction(PermissionActionName.Export)] - [HttpPost] - public async Task PdfInvoiceSelected(string selectedIds) - { - var orders = new List(); - if (selectedIds != null) - { - var ids = selectedIds - .Split([','], StringSplitOptions.RemoveEmptyEntries) - .Select(x => x) - .ToArray(); - orders.AddRange(await _orderService.GetOrdersByIds(ids)); - } - - //a vendor should have access only to his products - orders = orders.Where(_contextAccessor.WorkContext.HasAccessToOrder).ToList(); - - //ensure that we at least one order selected - if (orders.Count == 0) - { - Error(_translationService.GetResource("Vendor.Orders.PdfInvoice.NoOrders")); - return RedirectToAction("List"); - } - - byte[] bytes; - using (var stream = new MemoryStream()) - { - await _pdfService.PrintOrdersToPdf(stream, orders, _contextAccessor.WorkContext.WorkingLanguage.Id, - _contextAccessor.WorkContext.CurrentVendor.Id); - bytes = stream.ToArray(); - } - - return File(bytes, "application/pdf", "orders.pdf"); - } - - #endregion - - #endregion -} \ No newline at end of file +// Concrete host subclass of BaseOrderController (ARCH-001 Order consolidation). Vendor gets +// read-only/list/PDF actions only - it inherits BaseOrderController directly, never +// BaseOrderManagementController, so no mutating action exists on this type at all. This class +// supplies Vendor's DI wiring plus the attributes that used to arrive transitively via +// BaseVendorController - BaseOrderController 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 ProductController. +[AutoValidateAntiforgeryToken] +[Area(Constants.AreaVendor)] +[AuthorizeVendor] +[AuthorizeMenu] +public class OrderController( + IOrderViewModelService orderViewModelService, + IOrderService orderService, + ITranslationService translationService, + IContextAccessor contextAccessor, + IPdfService pdfService, + IAdminDataScope scope) + : BaseOrderController(orderViewModelService, orderService, translationService, contextAccessor, + pdfService, scope); diff --git a/src/Web/Grand.Web.Vendor/Interfaces/IOrderViewModelService.cs b/src/Web/Grand.Web.Vendor/Interfaces/IOrderViewModelService.cs deleted file mode 100644 index a1fb5d64a..000000000 --- a/src/Web/Grand.Web.Vendor/Interfaces/IOrderViewModelService.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Grand.Domain.Orders; -using Grand.Web.Vendor.Models.Orders; - -namespace Grand.Web.Vendor.Interfaces; - -public interface IOrderViewModelService -{ - Task PrepareOrderListModel(int? orderStatusId = null, int? paymentStatusId = null, - int? shippingStatusId = null, DateTime? startDate = null, string code = null); - - Task<(IEnumerable orderModels, int totalCount)> PrepareOrderModel(OrderListModel model, int pageIndex, - int pageSize); - - Task PrepareOrderDetailsModel(OrderModel model, Order order); - Task> PrepareOrders(OrderListModel model); -} \ No newline at end of file diff --git a/src/Web/Grand.Web.Vendor/Models/Orders/OrderListModel.cs b/src/Web/Grand.Web.Vendor/Models/Orders/OrderListModel.cs deleted file mode 100644 index e0c1b74ce..000000000 --- a/src/Web/Grand.Web.Vendor/Models/Orders/OrderListModel.cs +++ /dev/null @@ -1,66 +0,0 @@ -using Grand.Infrastructure.ModelBinding; -using Grand.Infrastructure.Models; -using Microsoft.AspNetCore.Mvc.Rendering; -using System.ComponentModel.DataAnnotations; - -namespace Grand.Web.Vendor.Models.Orders; - -public class OrderListModel : BaseModel -{ - [GrandResourceDisplayName("Vendor.Orders.List.StartDate")] - [UIHint("DateNullable")] - public DateTime? StartDate { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.List.EndDate")] - [UIHint("DateNullable")] - public DateTime? EndDate { get; set; } - - public string CustomerId { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.List.BillingEmail")] - public string BillingEmail { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.List.BillingLastName")] - public string BillingLastName { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.List.OrderStatus")] - public int OrderStatusId { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.List.PaymentStatus")] - public int PaymentStatusId { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.List.ShippingStatus")] - public int ShippingStatusId { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.List.PaymentMethod")] - public string PaymentMethodSystemName { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.List.Warehouse")] - public string WarehouseId { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.List.Product")] - public string ProductId { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.List.BillingCountry")] - public string BillingCountryId { get; set; } - - - [GrandResourceDisplayName("Vendor.Orders.List.OrderGuid")] - - public string OrderGuid { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.List.GoDirectlyToNumber")] - - public string GoDirectlyToNumber { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.List.OrderTagId")] - public string OrderTag { get; set; } - - public IList AvailableOrderStatuses { get; set; } = new List(); - public IList AvailablePaymentStatuses { get; set; } = new List(); - public IList AvailableShippingStatuses { get; set; } = new List(); - public IList AvailableWarehouses { get; set; } = new List(); - public IList AvailablePaymentMethods { get; set; } = new List(); - public IList AvailableCountries { get; set; } = new List(); - public IList AvailableOrderTags { get; set; } = new List(); -} \ No newline at end of file diff --git a/src/Web/Grand.Web.Vendor/Models/Orders/OrderModel.cs b/src/Web/Grand.Web.Vendor/Models/Orders/OrderModel.cs deleted file mode 100644 index c222e756f..000000000 --- a/src/Web/Grand.Web.Vendor/Models/Orders/OrderModel.cs +++ /dev/null @@ -1,149 +0,0 @@ -using Grand.Domain.Catalog; -using Grand.Domain.Payments; -using Grand.Domain.Tax; -using Grand.Infrastructure.ModelBinding; -using Grand.Infrastructure.Models; -using Grand.Web.Vendor.Models.Common; -using System.ComponentModel.DataAnnotations; - -namespace Grand.Web.Vendor.Models.Orders; - -public class OrderModel : BaseEntityModel -{ - //identifiers - [GrandResourceDisplayName("Vendor.Orders.Fields.ID")] - public override string Id { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.Fields.ID")] - public int OrderNumber { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.Fields.Code")] - public string Code { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.Fields.OrderGuid")] - public Guid OrderGuid { get; set; } - - //store - [GrandResourceDisplayName("Vendor.Orders.Fields.Store")] - public string StoreName { get; set; } - - //customer info - [GrandResourceDisplayName("Vendor.Orders.Fields.Customer")] - public string CustomerInfo { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.Fields.CustomerEmail")] - public string CustomerEmail { get; set; } - - public string CustomerFullName { get; set; } - - //order status - [GrandResourceDisplayName("Admin.Orders.Fields.OrderStatus")] - public string OrderStatus { get; set; } - - public int OrderStatusId { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.Fields.Currency")] - public string CurrencyCode { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.Fields.CurrencyRate")] - [UIHint("DoubleN4")] - public double CurrencyRate { get; set; } - - public TaxDisplayType TaxDisplayType { get; set; } - - //payment info - [GrandResourceDisplayName("Vendor.Orders.Fields.PaymentStatus")] - public string PaymentStatus { get; set; } - - public PaymentStatus PaymentStatusEnum { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.Fields.PaymentMethod")] - public string PaymentMethod { get; set; } - - //shipping info - public bool IsShippable { get; set; } - public bool PickUpInStore { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.Fields.PickupAddress")] - public AddressModel PickupAddress { get; set; } - - - [GrandResourceDisplayName("Vendor.Orders.Fields.ShippingAddress")] - public AddressModel ShippingAddress { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.Fields.ShippingMethod")] - public string ShippingMethod { get; set; } - - public string ShippingAdditionDescription { get; set; } - public string ShippingAddressGoogleMapsUrl { get; set; } - public bool CanAddNewShipments { get; set; } - - //billing info - [GrandResourceDisplayName("Vendor.Orders.Fields.BillingAddress")] - public AddressModel BillingAddress { get; set; } - - [GrandResourceDisplayName("Vendor.Orders.Fields.VatNumber")] - public string VatNumber { get; set; } - - //items - public bool HasDownloadableProducts { get; set; } - public IList Items { get; set; } = new List(); - - //creation date - [GrandResourceDisplayName("Vendor.Orders.Fields.CreatedOn")] - public DateTime CreatedOn { get; set; } - - //checkout attributes - public string CheckoutAttributeInfo { get; set; } - - - #region Nested Classes - - public class OrderItemModel : BaseEntityModel - { - public string ProductId { get; set; } - public string ProductName { get; set; } - public string VendorName { get; set; } - public string Sku { get; set; } - - public string PictureThumbnailUrl { get; set; } - - public string UnitPriceInclTax { get; set; } - public string UnitPriceExclTax { get; set; } - public double UnitPriceInclTaxValue { get; set; } - public double UnitPriceExclTaxValue { get; set; } - - public int Quantity { get; set; } - public int OpenQty { get; set; } - public int CancelQty { get; set; } - public int ShipQty { get; set; } - public int ReturnQty { get; set; } - - public string DiscountInclTax { get; set; } - public string DiscountExclTax { get; set; } - public double DiscountInclTaxValue { get; set; } - public double DiscountExclTaxValue { get; set; } - - public string SubTotalInclTax { get; set; } - public string SubTotalExclTax { get; set; } - public double SubTotalInclTaxValue { get; set; } - public double SubTotalExclTaxValue { get; set; } - - public string AttributeInfo { get; set; } - public string RecurringInfo { get; set; } - - public IList MerchandiseReturnIds { get; set; } = new List(); - public IList PurchasedGiftVoucherIds { get; set; } = new List(); - - public bool IsDownload { get; set; } - public int DownloadCount { get; set; } - public DownloadActivationType DownloadActivationType { get; set; } - public bool IsDownloadActivated { get; set; } - public Guid LicenseDownloadGuid { get; set; } - - public string Commission { get; set; } - public double CommissionValue { get; set; } - } - - #endregion -} \ No newline at end of file diff --git a/src/Web/Grand.Web.Vendor/Services/OrderViewModelService.cs b/src/Web/Grand.Web.Vendor/Services/OrderViewModelService.cs deleted file mode 100644 index f1b81aa3f..000000000 --- a/src/Web/Grand.Web.Vendor/Services/OrderViewModelService.cs +++ /dev/null @@ -1,614 +0,0 @@ -using Grand.Business.Core.Interfaces.Catalog.Prices; -using Grand.Business.Core.Interfaces.Catalog.Products; -using Grand.Business.Core.Interfaces.Checkout.GiftVouchers; -using Grand.Business.Core.Interfaces.Checkout.Orders; -using Grand.Business.Core.Interfaces.Checkout.Payments; -using Grand.Business.Core.Interfaces.Checkout.Shipping; -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.Common.Stores; -using Grand.Business.Core.Interfaces.Customers; -using Grand.Business.Core.Interfaces.Storage; -using Grand.Domain.Catalog; -using Grand.Domain.Common; -using Grand.Domain.Directory; -using Grand.Domain.Media; -using Grand.Domain.Orders; -using Grand.Domain.Payments; -using Grand.Domain.Shipping; -using Grand.Domain.Tax; -using Grand.Infrastructure; -using Grand.Web.Common.Localization; -using Grand.Web.Vendor.Extensions; -using Grand.Web.Vendor.Interfaces; -using Grand.Web.Vendor.Models.Orders; -using Microsoft.AspNetCore.Mvc.Rendering; -using System.Net; - -namespace Grand.Web.Vendor.Services; - -public class OrderViewModelService : IOrderViewModelService -{ - #region Fields - - private readonly IOrderService _orderService; - private readonly IDateTimeService _dateTimeService; - private readonly IPriceFormatter _priceFormatter; - private readonly ITranslationService _translationService; - private readonly IContextAccessor _contextAccessor; - private readonly ICurrencyService _currencyService; - private readonly IPaymentService _paymentService; - private readonly ICountryService _countryService; - private readonly IProductService _productService; - private readonly IGiftVoucherService _giftVoucherService; - private readonly IDownloadService _downloadService; - private readonly IStoreService _storeService; - private readonly IVendorService _vendorService; - private readonly IAddressAttributeParser _addressAttributeParser; - private readonly IPictureService _pictureService; - private readonly IMerchandiseReturnService _merchandiseReturnService; - private readonly ICustomerService _customerService; - private readonly IWarehouseService _warehouseService; - private readonly CurrencySettings _currencySettings; - private readonly TaxSettings _taxSettings; - private readonly AddressSettings _addressSettings; - private readonly IOrderTagService _orderTagService; - private readonly IOrderStatusService _orderStatusService; - private readonly IEnumTranslationService _enumTranslationService; - - #endregion - - #region Ctor - - public OrderViewModelService(IOrderService orderService, - IDateTimeService dateTimeService, - IPriceFormatter priceFormatter, - ITranslationService translationService, - IContextAccessor contextAccessor, - ICurrencyService currencyService, - IPaymentService paymentService, - ICountryService countryService, - IProductService productService, - IGiftVoucherService giftVoucherService, - IDownloadService downloadService, - IStoreService storeService, - IVendorService vendorService, - IAddressAttributeParser addressAttributeParser, - IPictureService pictureService, - IMerchandiseReturnService merchandiseReturnService, - ICustomerService customerService, - IWarehouseService warehouseService, - CurrencySettings currencySettings, - TaxSettings taxSettings, - AddressSettings addressSettings, - IOrderTagService orderTagService, - IOrderStatusService orderStatusService, IEnumTranslationService enumTranslationService) - { - _orderService = orderService; - _dateTimeService = dateTimeService; - _priceFormatter = priceFormatter; - _translationService = translationService; - _contextAccessor = contextAccessor; - _currencyService = currencyService; - _paymentService = paymentService; - _countryService = countryService; - _productService = productService; - _giftVoucherService = giftVoucherService; - _downloadService = downloadService; - _storeService = storeService; - _vendorService = vendorService; - _addressAttributeParser = addressAttributeParser; - _pictureService = pictureService; - _merchandiseReturnService = merchandiseReturnService; - _warehouseService = warehouseService; - _currencySettings = currencySettings; - _taxSettings = taxSettings; - _addressSettings = addressSettings; - _customerService = customerService; - _orderTagService = orderTagService; - _orderStatusService = orderStatusService; - _enumTranslationService = enumTranslationService; - } - - #endregion - - public virtual async Task PrepareOrderListModel( - int? orderStatusId = null, - int? paymentStatusId = null, - int? shippingStatusId = null, - DateTime? startDate = null, - string code = null) - { - //order statuses - var statuses = await _orderStatusService.GetAll(); - var model = new OrderListModel { - AvailableOrderStatuses = statuses - .Select(x => new SelectListItem { Value = x.StatusId.ToString(), Text = x.Name }).ToList() - }; - model.AvailableOrderStatuses.Insert(0, - new SelectListItem { Text = _translationService.GetResource("Vendor.Common.All"), Value = " " }); - if (orderStatusId.HasValue) - { - //pre-select value? - var item = model.AvailableOrderStatuses.FirstOrDefault(x => x.Value == orderStatusId.Value.ToString()); - if (item != null) - item.Selected = true; - } - - //payment statuses - model.AvailablePaymentStatuses = _enumTranslationService.ToSelectList(PaymentStatus.Pending, false).ToList(); - model.AvailablePaymentStatuses.Insert(0, - new SelectListItem { Text = _translationService.GetResource("Vendor.Common.All"), Value = " " }); - if (paymentStatusId.HasValue) - { - //pre-select value? - var item = model.AvailablePaymentStatuses.FirstOrDefault(x => - x.Value == paymentStatusId.Value.ToString()); - if (item != null) - item.Selected = true; - } - - //order's tags - model.AvailableOrderTags.Add(new SelectListItem - { Text = _translationService.GetResource("Vendor.Common.All"), Value = " " }); - foreach (var s in await _orderTagService.GetAllOrderTags()) - model.AvailableOrderTags.Add(new SelectListItem { Text = s.Name, Value = s.Id }); - - //shipping statuses - model.AvailableShippingStatuses = _enumTranslationService.ToSelectList(ShippingStatus.Pending, false).ToList(); - model.AvailableShippingStatuses.Insert(0, - new SelectListItem { Text = _translationService.GetResource("Vendor.Common.All"), Value = " " }); - if (shippingStatusId.HasValue) - { - //pre-select value? - var item = model.AvailableShippingStatuses.FirstOrDefault(x => - x.Value == shippingStatusId.Value.ToString()); - if (item != null) - item.Selected = true; - } - - //warehouses - model.AvailableWarehouses.Add(new SelectListItem - { Text = _translationService.GetResource("Vendor.Common.All"), Value = " " }); - foreach (var w in await _warehouseService.GetAllWarehouses()) - model.AvailableWarehouses.Add(new SelectListItem { Text = w.Name, Value = w.Id }); - - //payment methods - model.AvailablePaymentMethods.Add(new SelectListItem - { Text = _translationService.GetResource("Vendor.Common.All"), Value = " " }); - foreach (var pm in await _paymentService.LoadAllPaymentMethods()) - model.AvailablePaymentMethods.Add(new SelectListItem { Text = pm.FriendlyName, Value = pm.SystemName }); - - //billing countries - foreach (var c in await _countryService.GetAllCountriesForBilling(showHidden: true)) - model.AvailableCountries.Add(new SelectListItem { Text = c.Name, Value = c.Id }); - - model.AvailableCountries.Insert(0, - new SelectListItem { Text = _translationService.GetResource("Vendor.Common.All"), Value = " " }); - - if (startDate.HasValue) - model.StartDate = startDate.Value; - - if (!string.IsNullOrEmpty(code)) - model.GoDirectlyToNumber = code; - - return model; - } - - public virtual async Task<(IEnumerable orderModels, int totalCount)> PrepareOrderModel( - OrderListModel model, int pageIndex, int pageSize) - { - 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).AddDays(1); - - int? orderStatus = model.OrderStatusId > 0 ? model.OrderStatusId : null; - var paymentStatus = model.PaymentStatusId > 0 ? (PaymentStatus?)model.PaymentStatusId : null; - var shippingStatus = - model.ShippingStatusId > 0 ? (ShippingStatus?)model.ShippingStatusId : null; - - //load orders - var orders = await _orderService.SearchOrders( - vendorId: _contextAccessor.WorkContext.CurrentVendor.Id, - customerId: model.CustomerId, - productId: model.ProductId, - warehouseId: model.WarehouseId, - paymentMethodSystemName: model.PaymentMethodSystemName, - createdFromUtc: startDateValue, - createdToUtc: endDateValue, - os: orderStatus, - ps: paymentStatus, - ss: shippingStatus, - billingEmail: model.BillingEmail, - billingLastName: model.BillingLastName, - billingCountryId: model.BillingCountryId, - orderGuid: model.OrderGuid, - orderCode: model.GoDirectlyToNumber, - pageIndex: pageIndex - 1, - pageSize: pageSize, - orderTagId: model.OrderTag); - - var primaryStoreCurrency = await _currencyService.GetCurrencyById(_currencySettings.PrimaryStoreCurrencyId); - if (primaryStoreCurrency == null) - throw new Exception("Cannot load primary store currency"); - - var status = await _orderStatusService.GetAll(); - var items = new List(); - foreach (var x in orders) - { - var store = await _storeService.GetStoreById(x.StoreId); - items.Add(new OrderModel { - Id = x.Id, - OrderNumber = x.OrderNumber, - Code = x.Code, - StoreName = store != null ? store.Shortcut : "Unknown", - CurrencyCode = x.CustomerCurrencyCode, - OrderStatus = status.FirstOrDefault(y => y.StatusId == x.OrderStatusId)?.Name, - OrderStatusId = x.OrderStatusId, - PaymentStatus = _enumTranslationService.GetTranslationEnum(x.PaymentStatusId), - CustomerEmail = x.BillingAddress?.Email, - CustomerFullName = $"{x.BillingAddress?.FirstName} {x.BillingAddress?.LastName}", - CreatedOn = _dateTimeService.ConvertToUserTime(x.CreatedOnUtc, DateTimeKind.Utc) - }); - } - - return (items, orders.TotalCount); - } - - public virtual async Task PrepareOrderDetailsModel(OrderModel model, Order order) - { - ArgumentNullException.ThrowIfNull(order); - ArgumentNullException.ThrowIfNull(model); - - model.Id = order.Id; - model.OrderNumber = order.OrderNumber; - model.Code = order.Code; - model.OrderStatusId = order.OrderStatusId; - model.OrderGuid = order.OrderGuid; - - var status = await _orderStatusService.GetAll(); - model.OrderStatus = status.FirstOrDefault(x => x.StatusId == order.OrderStatusId)?.Name; - - var store = await _storeService.GetStoreById(order.StoreId); - model.StoreName = store != null ? store.Shortcut : "Unknown"; - model.UserFields = order.UserFields; - - var customer = await _customerService.GetCustomerById(order.CustomerId); - if (customer != null) - model.CustomerInfo = !string.IsNullOrEmpty(customer.Email) - ? customer.Email - : _translationService.GetResource("Vendor.Customers.Guest"); - - model.VatNumber = order.VatNumber; - model.CreatedOn = _dateTimeService.ConvertToUserTime(order.CreatedOnUtc, DateTimeKind.Utc); - model.TaxDisplayType = _taxSettings.TaxDisplayType; - - #region Order totals - - var primaryStoreCurrency = await _currencyService.GetCurrencyByCode(order.PrimaryCurrencyCode) ?? - await _currencyService.GetCurrencyById(_currencySettings.PrimaryStoreCurrencyId); - - if (primaryStoreCurrency == null) - throw new Exception("Cannot load primary store currency"); - - var orderCurrency = await _currencyService.GetCurrencyByCode(order.CustomerCurrencyCode); - if (orderCurrency == null) - throw new Exception("Cannot load order currency"); - - model.CurrencyRate = order.CurrencyRate; - model.CurrencyCode = order.CustomerCurrencyCode; - - #endregion - - #region Payment info - - //payment method info - var pm = _paymentService.LoadPaymentMethodBySystemName(order.PaymentMethodSystemName); - model.PaymentMethod = pm != null ? pm.FriendlyName : order.PaymentMethodSystemName; - model.PaymentStatus = _enumTranslationService.GetTranslationEnum(order.PaymentStatusId); - model.PaymentStatusEnum = order.PaymentStatusId; - - #endregion - - #region Billing & shipping info - - model.BillingAddress = await order.BillingAddress.ToModel(_countryService); - model.BillingAddress.FormattedCustomAddressAttributes = - await _addressAttributeParser.FormatAttributes(_contextAccessor.WorkContext.WorkingLanguage, - order.BillingAddress.Attributes); - model.BillingAddress.NameEnabled = _addressSettings.NameEnabled; - model.BillingAddress.FirstNameEnabled = true; - model.BillingAddress.FirstNameRequired = true; - model.BillingAddress.LastNameEnabled = true; - model.BillingAddress.LastNameRequired = true; - model.BillingAddress.EmailEnabled = true; - model.BillingAddress.EmailRequired = true; - model.BillingAddress.CompanyEnabled = _addressSettings.CompanyEnabled; - model.BillingAddress.CompanyRequired = _addressSettings.CompanyRequired; - model.BillingAddress.VatNumberEnabled = _addressSettings.VatNumberEnabled; - model.BillingAddress.VatNumberRequired = _addressSettings.VatNumberRequired; - model.BillingAddress.CountryEnabled = _addressSettings.CountryEnabled; - model.BillingAddress.StateProvinceEnabled = _addressSettings.StateProvinceEnabled; - model.BillingAddress.CityEnabled = _addressSettings.CityEnabled; - model.BillingAddress.CityRequired = _addressSettings.CityRequired; - model.BillingAddress.StreetAddressEnabled = _addressSettings.StreetAddressEnabled; - model.BillingAddress.StreetAddressRequired = _addressSettings.StreetAddressRequired; - model.BillingAddress.StreetAddress2Enabled = _addressSettings.StreetAddress2Enabled; - model.BillingAddress.StreetAddress2Required = _addressSettings.StreetAddress2Required; - model.BillingAddress.ZipPostalCodeEnabled = _addressSettings.ZipPostalCodeEnabled; - model.BillingAddress.ZipPostalCodeRequired = _addressSettings.ZipPostalCodeRequired; - model.BillingAddress.PhoneEnabled = _addressSettings.PhoneEnabled; - model.BillingAddress.PhoneRequired = _addressSettings.PhoneRequired; - model.BillingAddress.FaxEnabled = _addressSettings.FaxEnabled; - model.BillingAddress.FaxRequired = _addressSettings.FaxRequired; - model.BillingAddress.NoteEnabled = _addressSettings.NoteEnabled; - - if (order.ShippingStatusId != ShippingStatus.ShippingNotRequired) - { - model.IsShippable = true; - - model.PickUpInStore = order.PickUpInStore; - if (!order.PickUpInStore) - { - if (order.ShippingAddress != null) - { - model.ShippingAddress = await order.ShippingAddress.ToModel(_countryService); - model.ShippingAddress.FormattedCustomAddressAttributes = - await _addressAttributeParser.FormatAttributes(_contextAccessor.WorkContext.WorkingLanguage, - order.ShippingAddress.Attributes); - model.ShippingAddress.NameEnabled = _addressSettings.NameEnabled; - model.ShippingAddress.FirstNameEnabled = true; - model.ShippingAddress.FirstNameRequired = true; - model.ShippingAddress.LastNameEnabled = true; - model.ShippingAddress.LastNameRequired = true; - model.ShippingAddress.EmailEnabled = true; - model.ShippingAddress.EmailRequired = true; - model.ShippingAddress.CompanyEnabled = _addressSettings.CompanyEnabled; - model.ShippingAddress.CompanyRequired = _addressSettings.CompanyRequired; - model.ShippingAddress.VatNumberEnabled = _addressSettings.VatNumberEnabled; - model.ShippingAddress.VatNumberRequired = _addressSettings.VatNumberRequired; - model.ShippingAddress.CountryEnabled = _addressSettings.CountryEnabled; - model.ShippingAddress.StateProvinceEnabled = _addressSettings.StateProvinceEnabled; - model.ShippingAddress.CityEnabled = _addressSettings.CityEnabled; - model.ShippingAddress.CityRequired = _addressSettings.CityRequired; - model.ShippingAddress.StreetAddressEnabled = _addressSettings.StreetAddressEnabled; - model.ShippingAddress.StreetAddressRequired = _addressSettings.StreetAddressRequired; - model.ShippingAddress.StreetAddress2Enabled = _addressSettings.StreetAddress2Enabled; - model.ShippingAddress.StreetAddress2Required = _addressSettings.StreetAddress2Required; - model.ShippingAddress.ZipPostalCodeEnabled = _addressSettings.ZipPostalCodeEnabled; - model.ShippingAddress.ZipPostalCodeRequired = _addressSettings.ZipPostalCodeRequired; - model.ShippingAddress.PhoneEnabled = _addressSettings.PhoneEnabled; - model.ShippingAddress.PhoneRequired = _addressSettings.PhoneRequired; - model.ShippingAddress.FaxEnabled = _addressSettings.FaxEnabled; - model.ShippingAddress.FaxRequired = _addressSettings.FaxRequired; - model.ShippingAddress.NoteEnabled = _addressSettings.NoteEnabled; - - model.ShippingAddressGoogleMapsUrl = - $"https://maps.google.com/maps?f=q&hl=en&ie=UTF8&oe=UTF8&geocode=&q={WebUtility.UrlEncode(order.ShippingAddress.Address1 + " " + order.ShippingAddress.ZipPostalCode + " " + order.ShippingAddress.City + " " + (!string.IsNullOrEmpty(order.ShippingAddress.CountryId) ? (await _countryService.GetCountryById(order.ShippingAddress.CountryId))?.Name : ""))}"; - } - } - else - { - if (order.PickupPoint is { Address: not null }) - { - model.PickupAddress = await order.PickupPoint.Address.ToModel(_countryService); - var country = await _countryService.GetCountryById(order.PickupPoint.Address.CountryId); - if (country != null) - model.PickupAddress.CountryName = country.Name; - } - } - - model.ShippingMethod = order.ShippingMethod; - model.ShippingAdditionDescription = order.ShippingOptionAttributeDescription; - model.CanAddNewShipments = false; - - foreach (var orderItem in order.OrderItems) - { - //we can ship only shippable products - if (!orderItem.IsShipEnabled) - continue; - - if (orderItem.OpenQty <= 0) - continue; - - model.CanAddNewShipments = true; - } - } - - #endregion - - #region Products - - model.CheckoutAttributeInfo = order.CheckoutAttributeDescription; - var hasDownloadableItems = false; - var products = order.OrderItems - .Where(orderItem => _contextAccessor.WorkContext.HasAccessToOrderItem(orderItem)) - .ToList(); - - foreach (var orderItem in products) - { - var product = await _productService.GetProductByIdIncludeArch(orderItem.ProductId); - - if (product == null) continue; - - if (product.IsDownload) - hasDownloadableItems = true; - - var orderItemModel = new OrderModel.OrderItemModel { - Id = orderItem.Id, - ProductId = orderItem.ProductId, - ProductName = product.Name, - Sku = orderItem.Sku, - Quantity = orderItem.Quantity, - OpenQty = orderItem.OpenQty, - CancelQty = orderItem.CancelQty, - ReturnQty = orderItem.ReturnQty, - ShipQty = orderItem.ShipQty, - IsDownload = product.IsDownload, - DownloadCount = orderItem.DownloadCount, - DownloadActivationType = product.DownloadActivationTypeId, - IsDownloadActivated = orderItem.IsDownloadActivated - }; - //picture - var orderItemPicture = await GetProductPicture(product, orderItem.Attributes); - orderItemModel.PictureThumbnailUrl = await _pictureService.GetPictureUrl(orderItemPicture, 75); - - //license file - if (!string.IsNullOrEmpty(orderItem.LicenseDownloadId)) - { - var licenseDownload = await _downloadService.GetDownloadById(orderItem.LicenseDownloadId); - if (licenseDownload != null) orderItemModel.LicenseDownloadGuid = licenseDownload.DownloadGuid; - } - - //vendor - var vendor = await _vendorService.GetVendorById(orderItem.VendorId); - orderItemModel.VendorName = vendor != null ? vendor.Name : ""; - - //unit price - orderItemModel.UnitPriceInclTaxValue = orderItem.UnitPriceInclTax; - orderItemModel.UnitPriceExclTaxValue = orderItem.UnitPriceExclTax; - orderItemModel.UnitPriceInclTax = - _priceFormatter.FormatPrice(orderItem.UnitPriceInclTax, orderCurrency); - orderItemModel.UnitPriceExclTax = - _priceFormatter.FormatPrice(orderItem.UnitPriceExclTax, orderCurrency); - //discounts - orderItemModel.DiscountInclTaxValue = orderItem.DiscountAmountInclTax; - orderItemModel.DiscountExclTaxValue = orderItem.DiscountAmountExclTax; - orderItemModel.DiscountInclTax = - _priceFormatter.FormatPrice(orderItem.DiscountAmountInclTax, orderCurrency); - orderItemModel.DiscountExclTax = - _priceFormatter.FormatPrice(orderItem.DiscountAmountExclTax, orderCurrency); - //subtotal - orderItemModel.SubTotalInclTaxValue = orderItem.PriceInclTax; - orderItemModel.SubTotalExclTaxValue = orderItem.PriceExclTax; - orderItemModel.SubTotalInclTax = _priceFormatter.FormatPrice(orderItem.PriceInclTax, orderCurrency); - orderItemModel.SubTotalExclTax = _priceFormatter.FormatPrice(orderItem.PriceExclTax, orderCurrency); - - if (order.PrimaryCurrencyCode != order.CustomerCurrencyCode) - { - orderItemModel.UnitPriceInclTax += - $" ({_priceFormatter.FormatPrice(orderItem.UnitPriceInclTax / order.CurrencyRate, primaryStoreCurrency)})"; - orderItemModel.UnitPriceExclTax += - $" ({_priceFormatter.FormatPrice(orderItem.UnitPriceExclTax / order.CurrencyRate, primaryStoreCurrency)})"; - orderItemModel.DiscountInclTax += - $" ({_priceFormatter.FormatPrice(orderItem.DiscountAmountInclTax / order.CurrencyRate, primaryStoreCurrency)})"; - orderItemModel.DiscountExclTax += - $" ({_priceFormatter.FormatPrice(orderItem.DiscountAmountExclTax / order.CurrencyRate, primaryStoreCurrency)})"; - orderItemModel.SubTotalInclTax += - $" ({_priceFormatter.FormatPrice(orderItem.PriceInclTax / order.CurrencyRate, primaryStoreCurrency)})"; - orderItemModel.SubTotalExclTax += - $" ({_priceFormatter.FormatPrice(orderItem.PriceExclTax / order.CurrencyRate, primaryStoreCurrency)})"; - } - - // commission - orderItemModel.CommissionValue = orderItem.Commission; - orderItemModel.Commission = _priceFormatter.FormatPrice(orderItem.Commission, orderCurrency); - - orderItemModel.AttributeInfo = orderItem.AttributeDescription; - if (product.IsRecurring) - orderItemModel.RecurringInfo = string.Format( - _translationService.GetResource("Vendor.Orders.Products.RecurringPeriod"), - product.RecurringCycleLength, - _enumTranslationService.GetTranslationEnum(product.RecurringCyclePeriodId), - product.RecurringTotalCycles); - - //merchandise returns - orderItemModel.MerchandiseReturnIds = - (await _merchandiseReturnService.SearchMerchandiseReturns(orderItemId: orderItem.Id)) - .Select(rr => rr.Id).ToList(); - //gift vouchers - orderItemModel.PurchasedGiftVoucherIds = - (await _giftVoucherService.GetGiftVouchersByPurchasedWithOrderItemId(orderItem.Id)) - .Select(gc => gc.Id).ToList(); - - model.Items.Add(orderItemModel); - } - - model.HasDownloadableProducts = hasDownloadableItems; - - #endregion - } - - public virtual async Task> PrepareOrders(OrderListModel model) - { - 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).AddDays(1); - - int? orderStatus = model.OrderStatusId > 0 ? model.OrderStatusId : null; - var paymentStatus = model.PaymentStatusId > 0 ? (PaymentStatus?)model.PaymentStatusId : null; - var shippingStatus = - model.ShippingStatusId > 0 ? (ShippingStatus?)model.ShippingStatusId : null; - - //load orders - var orders = await _orderService.SearchOrders( - vendorId: _contextAccessor.WorkContext.CurrentVendor.Id, - productId: model.ProductId, - warehouseId: model.WarehouseId, - paymentMethodSystemName: model.PaymentMethodSystemName, - createdFromUtc: startDateValue, - createdToUtc: endDateValue, - os: orderStatus, - ps: paymentStatus, - ss: shippingStatus, - billingEmail: model.BillingEmail, - billingLastName: model.BillingLastName, - billingCountryId: model.BillingCountryId, - orderGuid: model.OrderGuid); - - return orders; - } - - private async Task GetProductPicture(Product product, IList attributes) - { - ArgumentNullException.ThrowIfNull(product); - - Picture picture = null; - - if (attributes != null && attributes.Any()) - { - var comb = product.FindProductAttributeCombination(attributes); - if (comb != null) - if (!string.IsNullOrEmpty(comb.PictureId)) - { - var combPicture = await _pictureService.GetPictureById(comb.PictureId); - if (combPicture != null) picture = combPicture; - } - - if (picture == null) - { - var attributeValues = product.ParseProductAttributeValues(attributes); - foreach (var attributeValue in attributeValues) - { - var attributePicture = await _pictureService.GetPictureById(attributeValue.PictureId); - if (attributePicture != null) - { - picture = attributePicture; - break; - } - } - } - } - - if (picture == null) - { - var pp = product.ProductPictures.OrderByDescending(p => p.IsDefault) - .ThenBy(p => p.DisplayOrder) - .FirstOrDefault(); - if (pp != null) - picture = await _pictureService.GetPictureById(pp.PictureId); - } - - return picture; - } - -} \ 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 a0da106c1..3bcacfa8c 100644 --- a/src/Web/Grand.Web.Vendor/Startup/StartupApplication.cs +++ b/src/Web/Grand.Web.Vendor/Startup/StartupApplication.cs @@ -23,7 +23,7 @@ public void ConfigureServices(IServiceCollection services, IConfiguration config // (Grand.Web.AdminShared/Startup/StartupApplication.cs), which is discovered and run for this // host too via the IStartupApplication assembly scan in StartupBase, since Vendor references // AdminShared. Registering it again here would just be a redundant duplicate of that line. - services.AddScoped(); + // IOrderViewModelService is likewise registered by Grand.Web.AdminShared's StartupApplication. services.AddScoped(); services.AddScoped(); services.AddScoped();