Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Expand Up @@ -43,6 +43,8 @@ public interface ICustomerService
/// <param name="pageIndex">Page index</param>
/// <param name="pageSize">Page size</param>
/// <param name="orderBySelector"></param>
/// <param name="excludedCustomerGroupIds">Customers in any of these groups are left out</param>
/// <param name="excludeVendorAndStaffAccounts">Leaves out accounts tied to a vendor or a staff store</param>
/// <returns>Customers</returns>
Task<IPagedList<Customer>> GetAllCustomers(DateTime? createdFromUtc = null,
DateTime? createdToUtc = null, string affiliateId = "", string vendorId = "", string storeId = "",
Expand All @@ -52,7 +54,8 @@ Task<IPagedList<Customer>> GetAllCustomers(DateTime? createdFromUtc = null,
string firstName = null, string lastName = null,
string company = null, string phone = null, string zipPostalCode = null,
bool loadOnlyWithShoppingCart = false, ShoppingCartType? sct = null,
int pageIndex = 0, int pageSize = int.MaxValue, Expression<Func<Customer, object>> orderBySelector = null);
int pageIndex = 0, int pageSize = int.MaxValue, Expression<Func<Customer, object>> orderBySelector = null,
string[] excludedCustomerGroupIds = null, bool excludeVendorAndStaffAccounts = false);


/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ public class GetCustomerQuery : IRequest<IPagedList<Customer>>
public string SalesEmployeeId { get; set; } = "";
public string[] CustomerGroupIds { get; set; } = null;
public string[] CustomerTagIds { get; set; } = null;

/// <summary>Customers in any of these groups are left out</summary>
public string[] ExcludedCustomerGroupIds { get; set; } = null;

/// <summary>Leaves out accounts tied to a vendor or a staff store</summary>
public bool ExcludeVendorAndStaffAccounts { get; set; } = false;

public string Email { get; set; } = null;
public string Username { get; set; } = null;
public string FirstName { get; set; } = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ public async Task<IPagedList<Customer>> Handle(GetCustomerQuery request, Cancell
query = query.Where(c => !c.Deleted);
if (request.CustomerGroupIds is { Length: > 0 })
query = query.Where(c => c.Groups.Any(x => request.CustomerGroupIds.Contains(x)));
if (request.ExcludedCustomerGroupIds is { Length: > 0 })
query = query.Where(c => !c.Groups.Any(x => request.ExcludedCustomerGroupIds.Contains(x)));
if (request.ExcludeVendorAndStaffAccounts)
query = query.Where(c => (c.VendorId == null || c.VendorId == "") &&
(c.StaffStoreId == null || c.StaffStoreId == ""));
if (request.CustomerTagIds is { Length: > 0 })
foreach (var item in request.CustomerTagIds)
query = query.Where(c => c.CustomerTags.Contains(item));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ public CustomerService(
/// <param name="pageIndex">Page index</param>
/// <param name="pageSize">Page size</param>
/// <param name="orderBySelector">order by selector</param>
/// <param name="excludedCustomerGroupIds">Customers in any of these groups are left out</param>
/// <param name="excludeVendorAndStaffAccounts">Leaves out accounts tied to a vendor or a staff store</param>
/// <returns>Customers</returns>
public virtual async Task<IPagedList<Customer>> GetAllCustomers(DateTime? createdFromUtc = null,
DateTime? createdToUtc = null, string affiliateId = "", string vendorId = "", string storeId = "",
Expand All @@ -88,7 +90,8 @@ public virtual async Task<IPagedList<Customer>> GetAllCustomers(DateTime? create
string firstName = null, string lastName = null,
string company = null, string phone = null, string zipPostalCode = null,
bool loadOnlyWithShoppingCart = false, ShoppingCartType? sct = null,
int pageIndex = 0, int pageSize = 2147483647, Expression<Func<Customer, object>> orderBySelector = null)
int pageIndex = 0, int pageSize = 2147483647, Expression<Func<Customer, object>> orderBySelector = null,
string[] excludedCustomerGroupIds = null, bool excludeVendorAndStaffAccounts = false)
{
var queryModel = new GetCustomerQuery {
CreatedFromUtc = createdFromUtc,
Expand All @@ -111,7 +114,9 @@ public virtual async Task<IPagedList<Customer>> GetAllCustomers(DateTime? create
Sct = sct,
PageIndex = pageIndex,
PageSize = pageSize,
OrderBySelector = orderBySelector
OrderBySelector = orderBySelector,
ExcludedCustomerGroupIds = excludedCustomerGroupIds,
ExcludeVendorAndStaffAccounts = excludeVendorAndStaffAccounts
};
return await _mediator.Send(queryModel);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,9 @@ await dbContext.CreateIndex(_customerRepository,
OrderBuilder<Customer>.Create().Ascending(x => x.Email).Ascending(x => x.StoreId), "Email_StoreId");
await dbContext.CreateIndex(_customerRepository,
OrderBuilder<Customer>.Create().Ascending(x => x.Username).Ascending(x => x.StoreId), "Username_StoreId");
//an owner's sub-accounts
await dbContext.CreateIndex(_customerRepository,
OrderBuilder<Customer>.Create().Ascending(x => x.OwnerId), "OwnerId_1");
await dbContext.CreateIndex(_customerRepository,
OrderBuilder<Customer>.Create().Descending(x => x.CreatedOnUtc), "CreatedOnUtc");

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using Grand.Data;
using Grand.Domain.Customers;
using Grand.Infrastructure.Migrations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace Grand.Module.Migration.Migrations._2._5;

public class MigrationCustomerOwnerIndex : IMigration
{
private const string IndexName = "OwnerId_1";

public int Priority => 1;
public DbVersion Version => new(2, 5);
public Guid Identity => new("6f0d4b9e-3c1a-4e27-9a85-2b7e4c1d9f63");
public string Name => "Index on Customer owner 2.5";

public async Task<bool> UpgradeProcess(IServiceProvider serviceProvider)
{
var repository = serviceProvider.GetRequiredService<IRepository<Customer>>();
var databaseContext = serviceProvider.GetRequiredService<IDatabaseContext>();
var logService = serviceProvider.GetRequiredService<ILogger<MigrationCustomerOwnerIndex>>();

try
{
//an owner's sub-accounts are listed on the storefront and in the admin customer form, and the admin
//checks for them before linking an owner - each a scan of the whole customer collection without it
await databaseContext.CreateIndex(repository,
OrderBuilder<Customer>.Create().Ascending(x => x.OwnerId), IndexName);
}
catch (Exception ex)
{
logService.LogError(ex, "UpgradeProcess - CustomerOwnerIndex (2.5)");
return false;
}
Comment on lines +31 to +35

return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,41 @@ public async Task Handle_PageSizeTwo_ReturnsFirstPageAndTotal()
Assert.AreEqual(4, result.TotalCount);
Assert.AreEqual(2, result.TotalPages);
}

[TestMethod]
public async Task Handle_ExcludedGroupsAndStaffAccounts_LeftOutInTheQuery()
{
var repository = new MongoDBRepositoryTest<Customer>();
var customer = new Customer { Email = "customer@x.com", Groups = { "registered" } };
var wholesale = new Customer { Email = "wholesale@x.com", Groups = { "registered", "wholesale" } };
repository.Insert(customer);
repository.Insert(wholesale);
repository.Insert(new Customer { Email = "vendor-group@x.com", Groups = { "registered", "vendors" } });
repository.Insert(new Customer { Email = "manager-group@x.com", Groups = { "registered", "storemanager" } });
repository.Insert(new Customer { Email = "vendor-link@x.com", Groups = { "registered" }, VendorId = "v1" });
repository.Insert(new Customer { Email = "staff-link@x.com", Groups = { "registered" }, StaffStoreId = "s1" });
var queryHandler = new GetCustomerQueryHandler(repository);

var result = await queryHandler.Handle(new GetCustomerQuery {
ExcludedCustomerGroupIds = ["vendors", "storemanager"],
ExcludeVendorAndStaffAccounts = true
}, CancellationToken.None);

CollectionAssert.AreEquivalent(new[] { "customer@x.com", "wholesale@x.com" },
result.Select(c => c.Email).ToArray());
Assert.AreEqual(2, result.TotalCount);
}

[TestMethod]
public async Task Handle_NoExclusions_KeepsEveryone()
{
var repository = new MongoDBRepositoryTest<Customer>();
repository.Insert(new Customer { Groups = { "vendors" }, VendorId = "v1" });
repository.Insert(new Customer { StaffStoreId = "s1" });

var result = await new GetCustomerQueryHandler(repository).Handle(new GetCustomerQuery(),
CancellationToken.None);

Assert.AreEqual(2, result.TotalCount);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ namespace Grand.Web.Admin.Tests.Controllers;
[TestClass]
public class StoreCustomerDataScopeTests
{
private static StoreCustomerDataScope Build(string staffStoreId, bool isRegistered = true)
private static StoreCustomerDataScope Build(string staffStoreId, bool isRegistered = true,
Action<Mock<IGroupService>> groups = null)
{
var customer = new Customer { StaffStoreId = staffStoreId };
var workContextMock = new Mock<IWorkContext>();
Expand All @@ -20,10 +21,55 @@ private static StoreCustomerDataScope Build(string staffStoreId, bool isRegister

var groupServiceMock = new Mock<IGroupService>();
groupServiceMock.Setup(g => g.IsRegistered(It.IsAny<Customer>())).ReturnsAsync(isRegistered);
groups?.Invoke(groupServiceMock);

return new StoreCustomerDataScope(contextAccessorMock.Object, groupServiceMock.Object);
}

//a store manager manages registered customers only: a staff account in reach could be reset and taken over

[TestMethod]
[DataRow("admin")]
[DataRow("vendor")]
[DataRow("storemanager")]
[DataRow("salesmanager")]
public async Task HasAccess_StaffGroupMember_False(string role)
{
var scope = Build("store-1", groups: g => {
g.Setup(x => x.IsAdmin(It.IsAny<Customer>())).ReturnsAsync(role == "admin");
g.Setup(x => x.IsVendor(It.IsAny<Customer>())).ReturnsAsync(role == "vendor");
g.Setup(x => x.IsStoreManager(It.IsAny<Customer>())).ReturnsAsync(role == "storemanager");
g.Setup(x => x.IsSalesManager(It.IsAny<Customer>())).ReturnsAsync(role == "salesmanager");
});

Assert.IsFalse(await scope.HasAccess(new Customer { StoreId = "store-1" }));
}

[TestMethod]
public async Task HasAccess_TiedToAVendor_False()
{
var scope = Build("store-1");

Assert.IsFalse(await scope.HasAccess(new Customer { StoreId = "store-1", VendorId = "vendor-1" }));
}

[TestMethod]
public async Task HasAccess_TiedToAStaffStore_False()
{
var scope = Build("store-1");

Assert.IsFalse(await scope.HasAccess(new Customer { StoreId = "store-1", StaffStoreId = "store-1" }));
}

[TestMethod]
public void StaffGroupSystemNames_CoverEveryStaffRole()
{
CollectionAssert.AreEquivalent(new[] {
SystemCustomerGroupNames.Administrators, SystemCustomerGroupNames.Vendors,
SystemCustomerGroupNames.StoreManager, SystemCustomerGroupNames.SalesManager
}, StoreCustomerDataScope.StaffGroupSystemNames);
}

[TestMethod]
public async Task HasAccess_MatchingStoreId_Registered_NotDeleted_True()
{
Expand Down
Loading
Loading