Skip to content

Customer Merge #7728 #7737

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
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
155 changes: 105 additions & 50 deletions src/Libraries/Nop.Services/Customers/CustomerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -307,15 +307,15 @@ public virtual async Task<IPagedList<Customer>> GetCustomersWithShoppingCartsAsy
//filter customers by billing country
if (countryId > 0)
customers = from c in customers
join a in _customerAddressRepository.Table on c.BillingAddressId equals a.Id
where a.CountryId == countryId
select c;
join a in _customerAddressRepository.Table on c.BillingAddressId equals a.Id
where a.CountryId == countryId
select c;

var customersWithCarts = from c in customers
join item in items on c.Id equals item.CustomerId
//we change ordering for the MySQL engine to avoid problems with the ONLY_FULL_GROUP_BY server property that is set by default since the 5.7.5 version
orderby _dataProvider.ConfigurationName == "MySql" ? c.CreatedOnUtc : item.CreatedOnUtc descending
select c;
join item in items on c.Id equals item.CustomerId
//we change ordering for the MySQL engine to avoid problems with the ONLY_FULL_GROUP_BY server property that is set by default since the 5.7.5 version
orderby _dataProvider.ConfigurationName == "MySql" ? c.CreatedOnUtc : item.CreatedOnUtc descending
select c;

return await customersWithCarts.Distinct().ToPagedListAsync(pageIndex, pageSize);
}
Expand Down Expand Up @@ -401,8 +401,8 @@ public virtual async Task<IList<Customer>> GetCustomersByGuidsAsync(Guid[] custo
return null;

var query = from c in _customerRepository.Table
where customerGuids.Contains(c.CustomerGuid)
select c;
where customerGuids.Contains(c.CustomerGuid)
select c;
var customers = await query.ToListAsync();

return customers;
Expand All @@ -422,9 +422,9 @@ public virtual async Task<Customer> GetCustomerByGuidAsync(Guid customerGuid)
return null;

var query = from c in _customerRepository.Table
where c.CustomerGuid == customerGuid
orderby c.Id
select c;
where c.CustomerGuid == customerGuid
orderby c.Id
select c;

return await _shortTermCacheManager.GetAsync(async () => await query.FirstOrDefaultAsync(), NopCustomerServicesDefaults.CustomerByGuidCacheKey, customerGuid);
}
Expand All @@ -443,9 +443,9 @@ public virtual async Task<Customer> GetCustomerByEmailAsync(string email)
return null;

var query = from c in _customerRepository.Table
orderby c.Id
where c.Email == email
select c;
orderby c.Id
where c.Email == email
select c;
var customer = await query.FirstOrDefaultAsync();

return customer;
Expand All @@ -465,9 +465,9 @@ public virtual async Task<Customer> GetCustomerBySystemNameAsync(string systemNa
return null;

var query = from c in _customerRepository.Table
orderby c.Id
where c.SystemName == systemName
select c;
orderby c.Id
where c.SystemName == systemName
select c;

var customer = await _shortTermCacheManager.GetAsync(async () => await query.FirstOrDefaultAsync(), NopCustomerServicesDefaults.CustomerBySystemNameCacheKey, systemName);

Expand Down Expand Up @@ -564,9 +564,9 @@ public virtual async Task<Customer> GetCustomerByUsernameAsync(string username)
return null;

var query = from c in _customerRepository.Table
orderby c.Id
where c.Username == username
select c;
orderby c.Id
where c.Username == username
select c;
var customer = await query.FirstOrDefaultAsync();

return customer;
Expand Down Expand Up @@ -682,28 +682,28 @@ public virtual async Task<int> DeleteGuestCustomersAsync(DateTime? createdFromUt
var guestRole = await GetCustomerRoleBySystemNameAsync(NopCustomerDefaults.GuestsRoleName);

var allGuestCustomers = from guest in _customerRepository.Table
join ccm in _customerCustomerRoleMappingRepository.Table on guest.Id equals ccm.CustomerId
where ccm.CustomerRoleId == guestRole.Id
select guest;
join ccm in _customerCustomerRoleMappingRepository.Table on guest.Id equals ccm.CustomerId
where ccm.CustomerRoleId == guestRole.Id
select guest;

var guestsToDelete = from guest in _customerRepository.Table
join g in allGuestCustomers on guest.Id equals g.Id
from sCart in _shoppingCartRepository.Table.Where(sci => sci.CustomerId == guest.Id).DefaultIfEmpty()
from order in _orderRepository.Table.Where(o => o.CustomerId == guest.Id).DefaultIfEmpty()
from blogComment in _blogCommentRepository.Table.Where(o => o.CustomerId == guest.Id).DefaultIfEmpty()
from newsComment in _newsCommentRepository.Table.Where(o => o.CustomerId == guest.Id).DefaultIfEmpty()
from productReview in _productReviewRepository.Table.Where(o => o.CustomerId == guest.Id).DefaultIfEmpty()
from productReviewHelpfulness in _productReviewHelpfulnessRepository.Table.Where(o => o.CustomerId == guest.Id).DefaultIfEmpty()
from pollVotingRecord in _pollVotingRecordRepository.Table.Where(o => o.CustomerId == guest.Id).DefaultIfEmpty()
from forumTopic in _forumTopicRepository.Table.Where(o => o.CustomerId == guest.Id).DefaultIfEmpty()
from forumPost in _forumPostRepository.Table.Where(o => o.CustomerId == guest.Id).DefaultIfEmpty()
where (!onlyWithoutShoppingCart || sCart == null) &&
order == null && blogComment == null && newsComment == null && productReview == null && productReviewHelpfulness == null &&
pollVotingRecord == null && forumTopic == null && forumPost == null &&
!guest.IsSystemAccount &&
(createdFromUtc == null || guest.CreatedOnUtc > createdFromUtc) &&
(createdToUtc == null || guest.CreatedOnUtc < createdToUtc)
select new { CustomerId = guest.Id };
join g in allGuestCustomers on guest.Id equals g.Id
from sCart in _shoppingCartRepository.Table.Where(sci => sci.CustomerId == guest.Id).DefaultIfEmpty()
from order in _orderRepository.Table.Where(o => o.CustomerId == guest.Id).DefaultIfEmpty()
from blogComment in _blogCommentRepository.Table.Where(o => o.CustomerId == guest.Id).DefaultIfEmpty()
from newsComment in _newsCommentRepository.Table.Where(o => o.CustomerId == guest.Id).DefaultIfEmpty()
from productReview in _productReviewRepository.Table.Where(o => o.CustomerId == guest.Id).DefaultIfEmpty()
from productReviewHelpfulness in _productReviewHelpfulnessRepository.Table.Where(o => o.CustomerId == guest.Id).DefaultIfEmpty()
from pollVotingRecord in _pollVotingRecordRepository.Table.Where(o => o.CustomerId == guest.Id).DefaultIfEmpty()
from forumTopic in _forumTopicRepository.Table.Where(o => o.CustomerId == guest.Id).DefaultIfEmpty()
from forumPost in _forumPostRepository.Table.Where(o => o.CustomerId == guest.Id).DefaultIfEmpty()
where (!onlyWithoutShoppingCart || sCart == null) &&
order == null && blogComment == null && newsComment == null && productReview == null && productReviewHelpfulness == null &&
pollVotingRecord == null && forumTopic == null && forumPost == null &&
!guest.IsSystemAccount &&
(createdFromUtc == null || guest.CreatedOnUtc > createdFromUtc) &&
(createdToUtc == null || guest.CreatedOnUtc < createdToUtc)
select new { CustomerId = guest.Id };

await using var tmpGuests = await _dataProvider.CreateTempDataStorageAsync("tmp_guestsToDelete", guestsToDelete);
await using var tmpAddresses = await _dataProvider.CreateTempDataStorageAsync("tmp_guestsAddressesToDelete",
Expand Down Expand Up @@ -1215,9 +1215,9 @@ public virtual async Task<CustomerRole> GetCustomerRoleBySystemNameAsync(string
var key = _staticCacheManager.PrepareKeyForDefaultCache(NopCustomerServicesDefaults.CustomerRolesBySystemNameCacheKey, systemName);

var query = from cr in _customerRoleRepository.Table
orderby cr.Id
where cr.SystemName == systemName
select cr;
orderby cr.Id
where cr.SystemName == systemName
select cr;

var customerRole = await _staticCacheManager.GetAsync(key, async () => await query.FirstOrDefaultAsync());

Expand Down Expand Up @@ -1615,9 +1615,9 @@ public virtual async Task InsertCustomerAddressAsync(Customer customer, Address
public virtual async Task<IList<Address>> GetAddressesByCustomerIdAsync(int customerId)
{
var query = from address in _customerAddressRepository.Table
join cam in _customerAddressMappingRepository.Table on address.Id equals cam.AddressId
where cam.CustomerId == customerId
select address;
join cam in _customerAddressMappingRepository.Table on address.Id equals cam.AddressId
where cam.CustomerId == customerId
select address;

return await _shortTermCacheManager.GetAsync(async () => await query.ToListAsync(), NopCustomerServicesDefaults.CustomerAddressesCacheKey, customerId);
}
Expand All @@ -1637,9 +1637,9 @@ public virtual async Task<Address> GetCustomerAddressAsync(int customerId, int a
return null;

var query = from address in _customerAddressRepository.Table
join cam in _customerAddressMappingRepository.Table on address.Id equals cam.AddressId
where cam.CustomerId == customerId && address.Id == addressId
select address;
join cam in _customerAddressMappingRepository.Table on address.Id equals cam.AddressId
where cam.CustomerId == customerId && address.Id == addressId
select address;

return await _shortTermCacheManager.GetAsync(async () => await query.FirstOrDefaultAsync(), NopCustomerServicesDefaults.CustomerAddressCacheKey, customerId, addressId);
}
Expand Down Expand Up @@ -1676,5 +1676,60 @@ public virtual async Task<Address> GetCustomerShippingAddressAsync(Customer cust

#endregion

#region Customer Merging

public virtual async Task MergeCustomersAsync(Customer fromCustomer, Customer toCustomer, bool deleteFromCustomer = true)
{

var fromOrders = await (from o in _orderRepository.Table
where o.CustomerId == fromCustomer.Id
select o).ToListAsync();

foreach (var order in fromOrders)
{
order.CustomerId = toCustomer.Id;
await _orderRepository.UpdateAsync(order);
}

var fromAddresses = await (from a in _customerAddressMappingRepository.Table
where a.CustomerId == fromCustomer.Id
select a).ToListAsync();

foreach (var address in fromAddresses)
{
address.CustomerId = toCustomer.Id;
await _customerAddressMappingRepository.InsertAsync(new CustomerAddressMapping() { AddressId = address.AddressId, CustomerId = toCustomer.Id });
}

await _customerAddressMappingRepository.DeleteAsync(fromAddresses);

if (fromAddresses.Any())
await _staticCacheManager.RemoveByPrefixAsync(NopCustomerServicesDefaults.CustomerAddressesByCustomerPrefix, toCustomer);

var anyRolesModified = false;
foreach (var role in await GetCustomerRolesAsync(fromCustomer, true))
{
if (!(await IsInCustomerRoleAsync(toCustomer, role.SystemName)))
{
await AddCustomerRoleMappingAsync(new CustomerCustomerRoleMapping
{
CustomerId = toCustomer.Id,
CustomerRoleId = role.Id
});
anyRolesModified = true;
}
}

if (anyRolesModified)
await _staticCacheManager.RemoveByPrefixAsync(NopCustomerServicesDefaults.CustomerCustomerRolesPrefix);

if (deleteFromCustomer)
await _customerRepository.DeleteAsync(fromCustomer);

}

#endregion


#endregion
}
6 changes: 6 additions & 0 deletions src/Libraries/Nop.Services/Customers/ICustomerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -648,4 +648,10 @@ Task<IList<CustomerPassword>> GetCustomerPasswordsAsync(int? customerId = null,
Task InsertCustomerAddressAsync(Customer customer, Address address);

#endregion

#region Customer Merging

Task MergeCustomersAsync(Customer fromCustomer, Customer toCustomer, bool deleteFromCustomer = true);

#endregion
}
Original file line number Diff line number Diff line change
Expand Up @@ -11904,6 +11904,18 @@
<LocaleResource Name="Admin.Customers.Customers.Addresses.Updated">
<Value>The address has been updated successfully.</Value>
</LocaleResource>
<LocaleResource Name="Admin.Customers.Customers.MergeCustomer">
<Value>Merge Customer</Value>
</LocaleResource>
<LocaleResource Name="Admin.Customers.Customers.MergeCustomerError">
<Value>Requested merge customer does not exist, has been deleted or already merged.</Value>
</LocaleResource>
<LocaleResource Name="Admin.Customers.Customers.MergeCustomerError.SameCustomers">
<Value>The same customer cannot be merged into itself.</Value>
</LocaleResource>
<LocaleResource Name="Admin.Customers.CustomerMerge.Fields.DeleteMergedCustomer">
<Value>Delete customer after Merge</Value>
</LocaleResource>
<LocaleResource Name="Admin.Customers.Customers.AdminAccountShouldExists.Deactivate">
<Value>You can't deactivate the last administrator. At least one administrator account should exists.</Value>
</LocaleResource>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,84 @@ await _customerActivityService.InsertActivityAsync("AddNewCustomer",
return View(model);
}

[HttpGet]
[CheckPermission(StandardPermission.Customers.CUSTOMERS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> Merge(int id)
{
if (await _customerService.GetCustomerByIdAsync(id) is Customer customer)
{
var customerModel = await _customerModelFactory.PrepareCustomerModelAsync(new CustomerModel(), customer);
customerModel.FullName = await _customerService.GetCustomerFullNameAsync(customer);
return View(new CustomerMergeModel(customerModel));
}

_notificationService.WarningNotification(await _localizationService.GetResourceAsync("Admin.Customers.Customers.MergeCustomerError"));
return RedirectToAction("List");
}

[HttpPost]
[CheckPermission(StandardPermission.Customers.CUSTOMERS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> MergeCustomerSearch(int? id, CustomerMergeModel searchModel)
{
if (id.HasValue)
{
if (await _customerService.GetCustomerByIdAsync(id.Value) is Customer customer)
{
var fullName = await _customerService.GetCustomerFullNameAsync(customer);
return Json(new { id = customer.Id, fullName = fullName, email = customer.Email });
}
else
{
return new NullJsonResult();
}
}
else
{
searchModel.SelectedCustomerRoleIds = new List<int> { (await _customerService.GetCustomerRoleBySystemNameAsync(NopCustomerDefaults.RegisteredRoleName)).Id };
var model = searchModel == null ? new() : await _customerModelFactory.PrepareCustomerListModelAsync(searchModel);
model.Data = model.Data.Where(c => c.Id != searchModel.CurrentCustomerId).ToList(); //exclude the customer being merged from the list
return Json(model);
}
}

[HttpPost]
[CheckPermission(StandardPermission.Customers.CUSTOMERS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> Merge(CustomerMergeModel model)
{
var mergeModel = model.Merge;
if (await _customerService.GetCustomerByIdAsync(mergeModel.FromId) is Customer fromCustomer &&
!fromCustomer.Deleted &&
await _customerService.GetCustomerByIdAsync(mergeModel.ToId) is Customer toCustomer &&
!toCustomer.Deleted)
{
if (fromCustomer.Id == toCustomer.Id)
{
//confirm that we are not trying to merge the same customer
_notificationService.WarningNotification(await _localizationService.GetResourceAsync("Admin.Customers.Customers.MergeCustomerError.SameCustomers"));
return RedirectToAction("Merge", new { id = mergeModel.FromId });
}

int resultId;
if (mergeModel.FromIsSource)
{
await _customerService.MergeCustomersAsync(fromCustomer, toCustomer, mergeModel.DeleteMergedCustomer);
resultId = toCustomer.Id;
}
else
{
await _customerService.MergeCustomersAsync(toCustomer, fromCustomer, mergeModel.DeleteMergedCustomer);
resultId = fromCustomer.Id;
}
return RedirectToAction("Edit", new { id = resultId });
}
else
{
//should be locale resource
_notificationService.WarningNotification(await _localizationService.GetResourceAsync("Admin.Customers.Customers.MergeCustomerError"));
return RedirectToAction("Merge", new { id = mergeModel.FromId });
}
}

[CheckPermission(StandardPermission.Customers.CUSTOMERS_VIEW)]
public virtual async Task<IActionResult> Edit(int id)
{
Expand Down Expand Up @@ -877,7 +955,7 @@ public virtual async Task<IActionResult> Delete(int id)
foreach (var store in await _storeService.GetAllStoresAsync())
{
var subscription = await _newsLetterSubscriptionService.GetNewsLetterSubscriptionByEmailAndStoreIdAsync(customerEmail, store.Id);

if (subscription != null)
await _newsLetterSubscriptionService.DeleteNewsLetterSubscriptionAsync(subscription);
}
Expand Down Expand Up @@ -1525,7 +1603,7 @@ public virtual async Task<IActionResult> GdprExport(int id)
{
//log
//_gdprService.InsertLog(customer, 0, GdprRequestType.ExportData, await _localizationService.GetResource("Gdpr.Exported"));

//export
var store = await _storeContext.GetCurrentStoreAsync();
var bytes = await _exportManager.ExportCustomerGdprInfoToXlsxAsync(customer, store.Id);
Expand Down
Loading