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. Returnfalsefor 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
@Transactionalannotations 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.