Skip to content

Merging Duplicate Accounts

An admin merges accounts that share an email from a customer's Review duplicate accounts (see Customers). UserMergeService runs the merge. It decides which account is kept, combines what lives on the User itself (details, roles, promotions, places), and hands everything else to the services that own it.

Records a customer owns: UserMergeParticipant

Every service whose documents hold a user id the customer owns implements UserMergeParticipant, and the merge calls every implementation:

Participant Moves
OrderServiceImpl Orders; then combines the kept account's open baskets into one and its saved-for-later holders into one
SubscriptionServiceImpl, InvoiceServiceImpl Subscriptions, invoices
DonationService Donations and Gift Aid declarations
LikeService Likes (an item both liked, once)
PageViewHistoryService This site's page views
ReviewServiceImpl, FundraiserService, UserUploadServiceImpl Reviews, fundraiser requests, uploads
GuestServiceImpl Restaurant guest records
PosServiceImpl, PosDeviceServiceImpl POS profiles (two are combined into one), POS devices
FulfillerServiceImpl Fulfiller links
SiteUsageStatisticsServiceImpl Recently viewed products (two lists woven into one)
PromotionAssignmentService, PromotionRedemptionService Promotions assigned to the account (one assigned to both, once), and promotion uses, so per-customer limits still count them

Adding a document with a user id the customer owns? Implement UserMergeParticipant on its service:

  • reassignUser(from, to, siteContext) moves that site's records from the duplicate to the kept account. Where the site allows only one per account and both have one, combine them into the kept account's rather than dropping either.
  • hasRecords(userId) says whether the account still has any on any site. A duplicate that does isn't marked for deletion. Return false for browsing history the clean-up job can lose.

Without it, the clean-up job deletes the duplicate and your records are left pointing at a user that no longer exists. Audit fields that only record who did something (a review's history, who checked a ticket in) aren't owned records and stay as they are.

How a merge runs

  • One site. Only the merging site's records move.
  • One at a time per shop, under a Redis lock.
  • All or nothing on a replica set (production): the merge runs in its own Mongo transaction, retried when Mongo aborts it as transient. It isn't a transaction manager bean, so other @Transactional annotations stay inert. A standalone Mongo (local dev) can't run transactions, so there each step skips what's already done and a failed merge can be run again.
  • Up to 100 duplicates per merge (UserMergeService.MAX_DUPLICATES_PER_MERGE), so a customer with hundreds of guest-checkout accounts doesn't outlast a transaction.
  • The kept account is saved before the participants run. They load it and update it in place (orders link its basket and saved-for-later holder), which a later whole-document save would undo.
  • Only the same email. A duplicate's roles move to the kept account, so only accounts with the kept account's email can be merged in, and a superadmin can't be.