Fix Event Participant Contact→User Mapping — Investigation & Fix Plan¶
Overview¶
Some participants in the eventParticipantRelatedList component display as grey (non-clickable) names instead of profile links. The component uses Contact.User_Lookup__c to resolve a Contact to a User for the profile link. When User_Lookup__c is null, hasUser = false and the name renders as plain grey text instead of a link.
Confirmed Example:
- Contact: Dillon Oergel (
003Um00001SEmTxIAL),Email = doergel1@gmail.com - User:
005Um00000B6gflIAB,Email = doergel1@gmail.com,Username = doergel1@gmail.com.smi Contact.User_Lookup__c = null— yet a perfectly matching User exists
Root Cause¶
The Contact.User_Lookup__c field is never automatically populated. The sync methods (syncContactToUser, bulkSyncContactsToUsers) exist in EventParticipantRedirectHelper.cls but are never called automatically — they must be triggered explicitly. No Flow or trigger populates this field on Contact create/update.
When getEventParticipants runs, it reads Contact__r.User_Lookup__c directly. If that field is null, hasUser = false and no profile link is rendered — even when a matching User exists.
Matching Logic (in syncContactToUser)¶
The sync uses a two-step prioritized match:
Contact.Email = User.Email(primary)Contact.Email + '.smi' = User.Username(fallback)
For Dillon Oergel, both would match — the sync was simply never run.
Scope of the Problem¶
A SOQL query against the org confirms this affects multiple contacts:
SELECT Id, Name, Email, User_Lookup__c
FROM Contact
WHERE Id IN (SELECT Contact__c FROM Event_Participant__c WHERE Event_Registration__c != null)
AND User_Lookup__c = null
At least 10 affected contacts confirmed (partial result), including Dillon Oergel, Sigrid Lee, Tony Voss, and others.
Desired End State¶
- All existing event participants whose Contact has a null
User_Lookup__cbut a matching User record — display as clickable profile links. - New participants added in the future are auto-synced so they display correctly from the first page load.
What We're NOT Doing¶
- Not changing the matching logic in
syncContactToUser/bulkSyncContactsToUsers(it already works) - Not changing the
ParticipantWrapperor the wire service shape - Not changing how Users are matched (Email > Username.smi pattern stays)
- Not adding a separate manual "sync" button to the UI
Key File Locations¶
| File | Purpose |
|---|---|
force-app/main/default/classes/EventParticipantRedirectHelper.cls |
Apex — syncContactToUser, bulkSyncContactsToUsers, getEventParticipants |
force-app/main/default/classes/EventParticipantRedirectHelperTest.cls |
Apex tests |
force-app/main/default/lwc/eventParticipantRelatedList/eventParticipantRelatedList.js |
LWC — loadParticipants, participant rendering |
Implementation Plan¶
Phase 1: Data Fix — Backfill Existing Null Mappings¶
Run anonymous Apex to sync all existing contacts that are event participants with a null User_Lookup__c. This is a one-time repair and unblocks all currently-affected participants immediately.
Verification Query (run before and after):
// Before: count contacts with null User_Lookup__c that are participants
List<Contact> unsynced = [
SELECT Id, Name, Email
FROM Contact
WHERE Id IN (SELECT Contact__c FROM Event_Participant__c)
AND User_Lookup__c = null
AND Email != null
];
System.debug('Unsynced participant contacts: ' + unsynced.size());
Backfill Script (Anonymous Apex):
// Collect all participant contact IDs with null User_Lookup__c
List<Contact> contacts = [
SELECT Id
FROM Contact
WHERE Id IN (SELECT Contact__c FROM Event_Participant__c WHERE Event_Registration__c != null)
AND User_Lookup__c = null
AND Email != null
];
List<String> contactIds = new List<String>();
for (Contact c : contacts) {
contactIds.add(c.Id);
}
System.debug('Syncing ' + contactIds.size() + ' contacts');
// Use existing bulk sync method
List<EventParticipantRedirectHelper.SyncResult> results =
EventParticipantRedirectHelper.bulkSyncContactsToUsers(contactIds);
Integer successCount = 0;
Integer failCount = 0;
for (EventParticipantRedirectHelper.SyncResult r : results) {
if (r.success) successCount++;
else failCount++;
}
System.debug('Sync complete. Success: ' + successCount + ', Failed: ' + failCount);
Run with:
sf apex run --file scripts/backfill-contact-user-sync.apex --target-org smi
Phase 1 Success Criteria¶
- Backfill script runs without error
-
successCountis non-zero in debug output — 38 contacts synced, 0 failures - Dillon Oergel (
003Um00001SEmTxIAL) now hasUser_Lookup__c = 005Um00000B6gflIAB - Navigate to the event page in Experience Cloud — Dillon Oergel appears as a clickable link
Phase 2: Auto-Sync on Participant Load (Preventive)¶
After loadParticipants returns in the LWC, check for any participants with hasUser = false. If found, call bulkSyncContactsToUsers for those contacts. If any sync succeeds, reload the participants. This self-heals newly-added participants on the next page view.
Changes Required¶
1. eventParticipantRelatedList.js¶
Add import for bulkSyncContactsToUsers:
import bulkSyncContactsToUsers from "@salesforce/apex/EventParticipantRedirectHelper.bulkSyncContactsToUsers";
Add a private syncMissingUsers method:
async syncMissingUsers(eventId) {
const missing = this.participants
.filter((p) => !p.hasUser && p.contactId)
.map((p) => p.contactId);
if (missing.length === 0) return;
try {
const results = await bulkSyncContactsToUsers({ contactIds: missing });
const anySuccess = results.some((r) => r.success);
if (anySuccess) {
// Reload participants now that User_Lookup__c is populated
this._foundWorkingId = false;
this._isLoadingParticipants = false;
await this.loadParticipants(eventId);
}
} catch (e) {
// Sync failure is non-critical — participants still display as grey names
console.error("Error syncing missing users:", e);
}
}
In loadParticipants, after this.participants = [...participants] (line ~323), call syncMissingUsers:
this.participants = [...participants];
this._eventIdFromUrl = eventId;
this._foundWorkingId = true;
this.error = null;
// Async: try to resolve any missing user links without blocking render
this.syncMissingUsers(eventId);
Why async/non-blocking? The sync call updates Contact records and re-fetches participants. This should not delay the initial render. Users will see the list immediately; if a sync resolves a missing link, the list will re-render with the link shortly after.
2. No Apex changes needed for Phase 2¶
bulkSyncContactsToUsers already handles the sync. The wire service's cached version of getEventParticipants may return stale data — the manual loadParticipants call in syncMissingUsers bypasses the cache via imperative Apex.
Phase 2 Success Criteria¶
- LWC deploys without errors
- Add a new participant whose Contact has
User_Lookup__c = null(but a matching User exists) → the name initially shows grey, then updates to a link within a few seconds - No errors thrown or shown to the user during sync
- Participants who already have
User_Lookup__cpopulated are unaffected
Phase 3: Auto-Sync on addParticipant (Apex)¶
When an event leader adds a participant via the component, attempt to sync the Contact→User mapping at insert time. This prevents the two-step experience in Phase 2 for newly-added participants.
Changes Required¶
EventParticipantRedirectHelper.cls — addParticipant method¶
After insert newParticipant; and before the re-query, add:
// Attempt to sync Contact→User mapping for the new participant
// This is best-effort — failure does not prevent the participant from being added
try {
syncContactToUser(contactId);
} catch (Exception syncEx) {
System.debug('Contact→User sync failed (non-critical): ' + syncEx.getMessage());
}
The existing re-query already reads Contact__r.User_Lookup__c, so if the sync succeeds, the returned ParticipantWrapper will already have hasUser = true.
Phase 3 Success Criteria¶
- Apex deploys without errors — all 39 tests pass (100%)
- Add a new participant via the component whose Contact has
User_Lookup__c = null(but a matching User exists) → renders immediately as a link - Test: add participant whose Contact has NO matching User → still adds successfully, renders as grey name
Phase 4: Fix and Activate the User_to_Contact_Sync_Trigger Flow¶
Background — Why the Flow Never Worked¶
Two flows were built to handle this sync but neither is active:
| Flow | Status | Problem |
|---|---|---|
User_to_Contact_Sync_Trigger |
InvalidDraft |
Never activated; assignment logic is wrong |
Sync_User_to_Contact |
Draft |
Autolaunched with triggerType = None — no trigger, never runs |
User_to_Contact_Sync_Trigger is the intended automatic trigger (fires on User create/update), but it has two bugs:
-
Wrong trigger type: Configured as
RecordBeforeSaveon User. Before-save flows can only modify the triggering record — they cannot perform DML on related records like Contact. UpdatingContact.User_Lookup__crequires an after-save flow. -
Wrong assignment target: The
Assign_User_to_Contactelement assigns$Record.User_Lookup__c = $Record.Id, which writes back to the User record itself — not to the Contact. There is no Record Update element for the Contact at all, so even if activated,Contact.User_Lookup__cwould never be set.
Changes Required¶
User_to_Contact_Sync_Trigger.flow-meta.xml¶
The flow logic is otherwise sound — the Contact lookup by Contact.Email = User.Email OR Contact.Email = User.Username is correct. The following must change:
-
Change trigger type from
RecordBeforeSavetoRecordAfterSave:<triggerType>RecordAfterSave</triggerType> -
Fix the assignment target: Instead of assigning to
$Record.User_Lookup__c(the User), assign to a Contact SObject variable:- Add a Contact variable (e.g.
ContactToUpdate) - In the assignment:
ContactToUpdate.Id = Get_Contact_Record.Id,ContactToUpdate.User_Lookup__c = $Record.Id
- Add a Contact variable (e.g.
-
Add a Record Update element that writes
ContactToUpdateto the database (replacing the current connector fromAssign_User_to_Contactback toGet_Contact_Record). -
Activate the flow: Change
<status>InvalidDraft</status>to<status>Active</status>.
The corrected flow shape:
Start (After-Save, User CreateAndUpdate)
→ Get_Contact_Record (by email match)
→ Contact_Found? (decision)
Yes → Check_Need_Update (Contact.User_Lookup__c != User.Id?)
Yes → Assign ContactToUpdate (Id + User_Lookup__c)
→ Update_Contact_Record (DML)
No → [end]
Phase 4 Success Criteria¶
- Flow deploys without errors
- Flow status is
Activein Setup → Flows (activated manually in WUI; CLI metadata deploy sets structure but activation requires WUI confirmation) - Create a new community User whose email matches an existing Contact →
Contact.User_Lookup__cis populated automatically within seconds - Update an existing User's email to match a Contact with null
User_Lookup__c→ same result - No errors appear in Setup → Apex Jobs or Debug Logs for the flow
Testing Strategy¶
Manual Test Cases¶
| # | Scenario | Expected Result |
|---|---|---|
| 1 | Existing participant with null User_Lookup__c but matching User (e.g. Dillon Oergel after Phase 1) |
Renders as clickable link |
| 2 | Participant with null User_Lookup__c, no matching User |
Renders as grey non-clickable name |
| 3 | Participant with populated User_Lookup__c |
Renders as clickable link (unchanged) |
| 4 | Add participant (Phase 3): Contact has matching User | Immediately renders as link in returned wrapper |
| 5 | Add participant (Phase 3): Contact has no matching User | Adds successfully, renders as grey name |
| 6 | Page load with mixed participants (some synced, some not) after Phase 2 | All synced participants get links; unsynced get links after a brief delay |
| 7 | New User created with email matching a Contact (Phase 4) | Contact.User_Lookup__c auto-populated; no manual sync needed |
Apex Unit Tests to Add¶
In EventParticipantRedirectHelperTest.cls:
addParticipant_syncsContactToUser_whenMatchingUserExists— afteraddParticipantreturns, verifywrapper.hasUser == trueaddParticipant_succeeds_whenNoMatchingUser— Contact with no matching User still returns a wrapper withhasUser == false
Execution Order¶
- Phase 1 first — immediate data fix, unblocks current affected participants with zero code deploy risk
- Phase 3 next — Apex only, prevents the issue for all future
addParticipantcalls - Phase 4 next — fix and activate the trigger flow so all future User creates/updates auto-sync to Contact; this is the proper long-term prevention
- ~~Phase 2~~ — Dropped. Phases 1, 3, and 4 together fully cover the problem; the LWC auto-sync adds complexity without meaningful benefit now that the flow is active and
addParticipantsyncs inline.
References¶
- Component:
force-app/main/default/lwc/eventParticipantRelatedList/ - Apex:
force-app/main/default/classes/EventParticipantRedirectHelper.cls - Trigger flow (broken):
force-app/main/default/flows/User_to_Contact_Sync_Trigger.flow-meta.xml - Autolaunched sync flow (inactive):
force-app/main/default/flows/Sync_User_to_Contact.flow-meta.xml - Confirmed broken contact:
003Um00001SEmTxIAL(Dillon Oergel) - Confirmed matching user:
005Um00000B6gflIAB