Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
f3fe526
feat(arch001): add IReportDataScope interface
KrzysztofPajak Aug 28, 2026
8cbd928
feat(arch001): add AdminReportDataScope and StoreReportDataScope
KrzysztofPajak Aug 28, 2026
4feb4a7
feat(arch001): add VendorReportDataScope
KrzysztofPajak Aug 28, 2026
b6b59bb
feat(arch001): add RoutedReportDataScope and register IReportDataScope
KrzysztofPajak Aug 28, 2026
75c5aa1
feat(arch001): add BaseReportsController skeleton + Bestsellers region
KrzysztofPajak Aug 28, 2026
a42a51c
feat(arch001): add BaseReportsController NeverSoldReport + CountryRep…
KrzysztofPajak Aug 28, 2026
8eda024
feat(arch001): add vendorId parameter to ICustomerReportViewModelServ…
KrzysztofPajak Aug 28, 2026
1d89c93
feat(arch001): add BaseReportsController LowStockReport + Customer re…
KrzysztofPajak Aug 28, 2026
3f0e133
feat(arch001): add BaseFullReportsController skeleton + Bestsellers-b…
KrzysztofPajak Aug 28, 2026
88d8086
fix(arch001): revert RouteData null-guard from production BaseFullRep…
KrzysztofPajak Aug 28, 2026
86379d1
feat(arch001): add BaseFullReportsController customer-report region (…
KrzysztofPajak Aug 28, 2026
94c311b
feat(arch001): cut Admin/Store/Vendor ReportsController over to thin …
KrzysztofPajak Aug 28, 2026
b6b91f6
feat(arch001): migrate Admin+Store Reports views into Grand.Web.Admin…
KrzysztofPajak Aug 28, 2026
0d07b63
test(arch001): add Store/Vendor Reports routing-attribute tests + Ven…
KrzysztofPajak Aug 28, 2026
cf5849b
fix(arch001): split Vendor's action-surface guard into distinct BaseF…
KrzysztofPajak Aug 28, 2026
423d139
fix(arch001): close cross-tenant data leak in GetBestsellersBriefRepo…
KrzysztofPajak Aug 28, 2026
ddb29e9
Merge remote-tracking branch 'origin/develop' into arch001/phase9-rep…
KrzysztofPajak Aug 28, 2026
513048d
fix(arch001): harden BaseReportsController/BaseFullReportsController …
KrzysztofPajak Aug 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Grand.Domain.Catalog;
using Grand.Web.AdminShared.Services;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Grand.Web.Admin.Tests.Controllers;

[TestClass]
public class AdminReportDataScopeTests
{
[TestMethod]
public void ScopeDefaults_UnscopedWithBothSelectorsShown()
{
var scope = new AdminReportDataScope();

Assert.AreEqual("", scope.StoreId);
Assert.AreEqual("", scope.VendorId);
Assert.IsTrue(scope.ShowStoreSelector);
Assert.IsTrue(scope.ShowVendorSelector);
Assert.AreEqual("Admin", scope.ResourceKeyPrefix);
}

[TestMethod]
public void CanIncludeProduct_NotOverridden_AlwaysTrue()
{
var scope = new AdminReportDataScope();
Assert.IsTrue(scope.CanIncludeProduct(new Product { Id = "p1" }));
Assert.IsTrue(scope.CanIncludeProduct(null));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
#nullable enable

using Grand.Business.Core.Interfaces.Catalog.Directory;
using Grand.Business.Core.Interfaces.Catalog.Prices;
using Grand.Business.Core.Interfaces.Catalog.Products;
using Grand.Business.Core.Interfaces.Checkout.Orders;
using Grand.Business.Core.Interfaces.Common.Directory;
using Grand.Business.Core.Interfaces.Common.Localization;
using Grand.Business.Core.Interfaces.Common.Security;
using Grand.Business.Core.Interfaces.Common.Stores;
using Grand.Business.Core.Interfaces.Customers;
using Grand.Business.Core.Interfaces.System.Reports;
using Grand.Business.Core.Utilities.System;
using Grand.Domain;
using Grand.Domain.Directory;
using Grand.Domain.Orders;
using Grand.Domain.Permissions;
using Grand.Infrastructure;
using Grand.Web.AdminShared.Controllers;
using Grand.Web.AdminShared.Interfaces;
using Grand.Web.AdminShared.Models.Customers;
using Grand.Web.AdminShared.Models.Orders;
using Grand.Web.Common.DataSource;
using Grand.Web.Common.Localization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

namespace Grand.Web.Admin.Tests.Controllers;

[TestClass]
public class BaseFullReportsControllerTests
{
private class TestFullReportsController(
IOrderReportService orderReportService,
IProductsReportService productsReportService,
ICustomerReportViewModelService customerReportViewModelService,
IPriceFormatter priceFormatter,
ICurrencyService currencyService,
IProductService productService,
IProductAttributeFormatter productAttributeFormatter,
IStockQuantityService stockQuantityService,
ITranslationService translationService,
IStoreService storeService,
ICountryService countryService,
IVendorService vendorService,
IDateTimeService dateTimeService,
IOrderStatusService orderStatusService,
IEnumTranslationService enumTranslationService,
IContextAccessor contextAccessor,
IReportDataScope scope,
IOrderService orderService,
ICustomerReportService customerReportService,
IPermissionService permissionService)
: BaseFullReportsController(orderReportService, productsReportService, customerReportViewModelService,
priceFormatter, currencyService, productService, productAttributeFormatter, stockQuantityService,
translationService, storeService, countryService, vendorService, dateTimeService,
orderStatusService, enumTranslationService, contextAccessor, scope, orderService,
customerReportService, permissionService);

private TestFullReportsController _controller = null!;
private Mock<IOrderReportService> _orderReportServiceMock = null!;
private Mock<IOrderService> _orderServiceMock = null!;
private Mock<IPermissionService> _permissionServiceMock = null!;
private Mock<IReportDataScope> _scopeMock = null!;
private Mock<ICustomerReportViewModelService> _customerReportViewModelServiceMock = null!;
private Mock<ICustomerReportService> _customerReportServiceMock = null!;

[TestInitialize]
public void Setup()
{
_orderReportServiceMock = new Mock<IOrderReportService>();
var productsReportServiceMock = new Mock<IProductsReportService>();
_customerReportViewModelServiceMock = new Mock<ICustomerReportViewModelService>();
var priceFormatterMock = new Mock<IPriceFormatter>();
priceFormatterMock.Setup(p => p.FormatPrice(It.IsAny<double>(), It.IsAny<Currency>())).Returns("$0.00");
var currencyServiceMock = new Mock<ICurrencyService>();
currencyServiceMock.Setup(c => c.GetPrimaryStoreCurrency()).ReturnsAsync(new Currency());
var productServiceMock = new Mock<IProductService>();
var productAttributeFormatterMock = new Mock<IProductAttributeFormatter>();
var stockQuantityServiceMock = new Mock<IStockQuantityService>();
var translationServiceMock = new Mock<ITranslationService>();
translationServiceMock.Setup(t => t.GetResource(It.IsAny<string>())).Returns("resource");
var storeServiceMock = new Mock<IStoreService>();
var countryServiceMock = new Mock<ICountryService>();
var vendorServiceMock = new Mock<IVendorService>();
var dateTimeServiceMock = new Mock<IDateTimeService>();
var orderStatusServiceMock = new Mock<IOrderStatusService>();
orderStatusServiceMock.Setup(o => o.GetAll()).ReturnsAsync(new List<Grand.Domain.Orders.OrderStatus>());
var enumTranslationServiceMock = new Mock<IEnumTranslationService>();
var contextAccessorMock = new Mock<IContextAccessor>();
_scopeMock = new Mock<IReportDataScope>();
_scopeMock.Setup(s => s.StoreId).Returns("");
_scopeMock.Setup(s => s.VendorId).Returns("");
_orderServiceMock = new Mock<IOrderService>();
_customerReportServiceMock = new Mock<ICustomerReportService>();
_permissionServiceMock = new Mock<IPermissionService>();
_permissionServiceMock.Setup(p => p.Authorize(StandardPermission.ManageOrders)).ReturnsAsync(true);

_controller = new TestFullReportsController(_orderReportServiceMock.Object, productsReportServiceMock.Object,
_customerReportViewModelServiceMock.Object, priceFormatterMock.Object, currencyServiceMock.Object,
productServiceMock.Object, productAttributeFormatterMock.Object, stockQuantityServiceMock.Object,
translationServiceMock.Object, storeServiceMock.Object, countryServiceMock.Object,
vendorServiceMock.Object, dateTimeServiceMock.Object, orderStatusServiceMock.Object,
enumTranslationServiceMock.Object, contextAccessorMock.Object, _scopeMock.Object,
_orderServiceMock.Object, _customerReportServiceMock.Object, _permissionServiceMock.Object);

var httpContext = new DefaultHttpContext();
var urlHelperFactoryMock = new Mock<IUrlHelperFactory>();
urlHelperFactoryMock.Setup(f => f.GetUrlHelper(It.IsAny<ActionContext>())).Returns(new Mock<IUrlHelper>().Object);
var requestServicesMock = new Mock<IServiceProvider>();
requestServicesMock.Setup(s => s.GetService(typeof(IUrlHelperFactory))).Returns(urlHelperFactoryMock.Object);
httpContext.RequestServices = requestServicesMock.Object;
var routeData = new RouteData();
routeData.Values["area"] = "Admin";
_controller.ControllerContext = new ControllerContext { HttpContext = httpContext, RouteData = routeData };
}

[TestMethod]
public async Task BestsellersBriefReportByQuantityList_ManageOrdersDenied_ReturnsEmptyContent()
{
_permissionServiceMock.Setup(p => p.Authorize(StandardPermission.ManageOrders)).ReturnsAsync(false);

var result = await _controller.BestsellersBriefReportByQuantityList(new DataSourceRequest { Page = 1, PageSize = 10 }) as ContentResult;

Assert.IsNotNull(result);
Assert.AreEqual("", result!.Content);
_orderReportServiceMock.Verify(o => o.BestSellersReport(It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<DateTime?>(), It.IsAny<DateTime?>(), It.IsAny<int?>(), It.IsAny<Grand.Domain.Payments.PaymentStatus?>(),
It.IsAny<Grand.Domain.Shipping.ShippingStatus?>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>(),
It.IsAny<int>(), It.IsAny<bool>()), Times.Never);
}

[TestMethod]
public async Task BestsellersBriefReportByAmountList_ManageOrdersAllowed_DelegatesToBase()
{
_orderReportServiceMock.Setup(o => o.BestSellersReport("", "", null, null, null, null, null, "", 2, 0, 10, true))
.ReturnsAsync(new PagedList<BestsellersReportLine>(new List<BestsellersReportLine>(), 0, 0));

var result = await _controller.BestsellersBriefReportByAmountList(new DataSourceRequest { Page = 1, PageSize = 10 });

Assert.IsInstanceOfType(result, typeof(JsonResult));
}

[TestMethod]
public async Task BestsellersBriefReportByAmountList_ScopeValuesThreadedIntoQuery()
{
_scopeMock.Setup(s => s.StoreId).Returns("store-1");
_scopeMock.Setup(s => s.VendorId).Returns("vendor-1");
_orderReportServiceMock.Setup(o => o.BestSellersReport("store-1", "vendor-1", null, null, null, null, null,
"", 2, 0, 10, true))
.ReturnsAsync(new PagedList<BestsellersReportLine>(new List<BestsellersReportLine>(), 0, 0));

await _controller.BestsellersBriefReportByAmountList(new DataSourceRequest { Page = 1, PageSize = 10 });

_orderReportServiceMock.Verify(o => o.BestSellersReport("store-1", "vendor-1", null, null, null, null, null,
"", 2, 0, 10, true), Times.Once);
}

[TestMethod]
public async Task BestsellersBriefReportByAmountList_CanIncludeProductFalse_DropsRow()
{
var line = new BestsellersReportLine { ProductId = "p1", TotalAmount = 1, TotalQuantity = 1 };
_orderReportServiceMock.Setup(o => o.BestSellersReport("", "", null, null, null, null, null, "", 2, 0, 10, true))
.ReturnsAsync(new PagedList<BestsellersReportLine>(new List<BestsellersReportLine> { line }, 0, 1));
_scopeMock.Setup(s => s.CanIncludeProduct(It.IsAny<Grand.Domain.Catalog.Product>())).Returns(false);

var result = await _controller.BestsellersBriefReportByAmountList(new DataSourceRequest { Page = 1, PageSize = 10 }) as JsonResult;

var gridModel = (DataSourceResult)result!.Value!;
Assert.AreEqual(0, ((List<BestsellersReportLineModel>)gridModel.Data).Count);
}

[TestMethod]
public async Task ReportOrderPeriodList_ManageOrdersDenied_ReturnsEmptyContent()
{
_permissionServiceMock.Setup(p => p.Authorize(StandardPermission.ManageOrders)).ReturnsAsync(false);

var result = await _controller.ReportOrderPeriodList(new DataSourceRequest { Page = 1, PageSize = 10 }) as ContentResult;

Assert.IsNotNull(result);
Assert.AreEqual("", result!.Content);
}

[TestMethod]
public async Task ReportOrderTimeChart_ManageOrdersAllowed_PassesScopeStoreId()
{
_scopeMock.Setup(s => s.StoreId).Returns("store-1");
_orderReportServiceMock.Setup(o => o.GetOrderByTimeReport("store-1", null, null))
.ReturnsAsync(new List<OrderByTimeReportLine>());

await _controller.ReportOrderTimeChart(new DataSourceRequest { Page = 1, PageSize = 10 }, null, null);

_orderReportServiceMock.Verify(o => o.GetOrderByTimeReport("store-1", null, null), Times.Once);
}

[TestMethod]
public async Task OrderAverageReportList_ManageOrdersAllowed_UsesScopeStoreIdForAllFourStatuses()
{
_scopeMock.Setup(s => s.StoreId).Returns("store-1");
_orderReportServiceMock.Setup(o => o.OrderAverageReport("store-1", It.IsAny<int>()))
.ReturnsAsync(new OrderAverageReportLineSummary());

var result = await _controller.OrderAverageReportList(new DataSourceRequest { Page = 1, PageSize = 10 });

Assert.IsInstanceOfType(result, typeof(JsonResult));
_orderReportServiceMock.Verify(o => o.OrderAverageReport("store-1", It.IsAny<int>()), Times.Exactly(4));
}

[TestMethod]
public async Task ReportLatestOrder_ManageOrdersAllowed_PassesScopeStoreIdToSearchOrders()
{
_scopeMock.Setup(s => s.StoreId).Returns("store-1");
_orderServiceMock.Setup(o => o.SearchOrders("store-1", "", "", "", "", "", "", "", "",
null, null, null, null, null, null, null, "", null, null, 0, 10, ""))
.ReturnsAsync(new PagedList<Order>(new List<Order>(), 0, 0));

await _controller.ReportLatestOrder(new DataSourceRequest { Page = 1, PageSize = 10 }, null, null);

_orderServiceMock.Verify(o => o.SearchOrders("store-1", "", "", "", "", "", "", "", "",
null, null, null, null, null, null, null, "", null, null, 0, 10, ""), Times.Once);
}

[TestMethod]
public async Task OrderIncompleteReportList_ManageOrdersAllowed_ReturnsThreeRows()
{
_orderReportServiceMock.Setup(o => o.GetOrderAverageReportLine(It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<int?>(), It.IsAny<Grand.Domain.Payments.PaymentStatus?>(), It.IsAny<Grand.Domain.Shipping.ShippingStatus?>(),
It.IsAny<DateTime?>(), It.IsAny<DateTime?>(), It.IsAny<string>(), It.IsAny<string>(), true, It.IsAny<string>()))
.ReturnsAsync(new OrderAverageReportLine());

var result = await _controller.OrderIncompleteReportList(new DataSourceRequest { Page = 1, PageSize = 10 }) as JsonResult;

var gridModel = (DataSourceResult)result!.Value!;
Assert.AreEqual(3, ((List<OrderIncompleteReportLineModel>)gridModel.Data).Count);
}

[TestMethod]
public async Task ReportBestCustomersByNumberOfOrdersList_PassesScopeVendorIdToService()
{
_scopeMock.Setup(s => s.VendorId).Returns("vendor-1");
_customerReportViewModelServiceMock.Setup(s =>
s.PrepareBestCustomerReportLineModel(It.IsAny<BestCustomersReportModel>(), 2, 1, 10, "vendor-1"))
.ReturnsAsync((new List<BestCustomerReportLineModel>(), 0));

await _controller.ReportBestCustomersByNumberOfOrdersList(new DataSourceRequest { Page = 1, PageSize = 10 },
new BestCustomersReportModel());

_customerReportViewModelServiceMock.Verify(s =>
s.PrepareBestCustomerReportLineModel(It.IsAny<BestCustomersReportModel>(), 2, 1, 10, "vendor-1"), Times.Once);
}

/// <summary>Verifies both scope values are passed to the service call, not that vendorId changes
/// behavior — GetReportRegisteredCustomersModel's vendorId parameter is a documented no-op today
/// (see its XML doc / ICustomerReportViewModelService), kept for signature symmetry and
/// forward-compatibility. scope.VendorId is always "" for this Full-tier-only action anyway.</summary>
[TestMethod]
public async Task ReportRegisteredCustomersList_ScopeStoreIdAndVendorIdAcceptedByServiceCall()
{
_scopeMock.Setup(s => s.StoreId).Returns("store-1");
_scopeMock.Setup(s => s.VendorId).Returns("vendor-1");
_customerReportViewModelServiceMock.Setup(s => s.GetReportRegisteredCustomersModel("store-1", "vendor-1"))
.ReturnsAsync(new List<RegisteredCustomerReportLineModel>());

await _controller.ReportRegisteredCustomersList(new DataSourceRequest { Page = 1, PageSize = 10 });

_customerReportViewModelServiceMock.Verify(s => s.GetReportRegisteredCustomersModel("store-1", "vendor-1"), Times.Once);
}

[TestMethod]
public async Task ReportCustomerTimeChart_PassesScopeStoreIdToService()
{
_scopeMock.Setup(s => s.StoreId).Returns("store-1");
_customerReportServiceMock.Setup(s => s.GetCustomerByTimeReport("store-1", null, null))
.ReturnsAsync(new List<CustomerByTimeReportLine>());

await _controller.ReportCustomerTimeChart(new DataSourceRequest { Page = 1, PageSize = 10 }, null, null);

_customerReportServiceMock.Verify(s => s.GetCustomerByTimeReport("store-1", null, null), Times.Once);
}
}
Loading
Loading