Improved participant matching with additional emails support (#12368)
# Improved participant matching with additional emails support Closes #8991 This PR extends the participant matching system to support additional emails in addition to primary emails for both calendar events and messages. Previously, the system only matched participants based on primary emails, missing matches with secondary email addresses. - Contact creation now consider both primary and additional emails when checking for existing contacts - Calendar and message participant listeners now handle both primary and additional email changes - Added tests ## To test this PR: Check that: - Primary emails take precedence over additional emails in matching - Case-insensitive email comparisons work correctly - A contact is not created if a person already exists with the email as its additional email - Event listeners handle both creation and update scenarios - Matching and unmatching logic works for complex email change scenarios - When unmatching after a change in a primary or secondary email, events and messages should be rematched if another person has this email as its primary or secondary email. --------- Co-authored-by: guillim <guigloo@msn.com>
This commit is contained in:
@ -8,6 +8,7 @@ import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/
|
||||
|
||||
export type MessageParticipantMatchParticipantJobData = {
|
||||
workspaceId: string;
|
||||
isPrimaryEmail: boolean;
|
||||
email: string;
|
||||
personId?: string;
|
||||
workspaceMemberId?: string;
|
||||
@ -24,13 +25,25 @@ export class MessageParticipantMatchParticipantJob {
|
||||
|
||||
@Process(MessageParticipantMatchParticipantJob.name)
|
||||
async handle(data: MessageParticipantMatchParticipantJobData): Promise<void> {
|
||||
const { email, personId, workspaceMemberId } = data;
|
||||
const { isPrimaryEmail, email, personId, workspaceMemberId } = data;
|
||||
|
||||
await this.matchParticipantService.matchParticipantsAfterPersonOrWorkspaceMemberCreation(
|
||||
email,
|
||||
'messageParticipant',
|
||||
personId,
|
||||
workspaceMemberId,
|
||||
);
|
||||
if (personId) {
|
||||
await this.matchParticipantService.matchParticipantsAfterPersonCreation({
|
||||
handle: email,
|
||||
isPrimaryEmail,
|
||||
objectMetadataName: 'messageParticipant',
|
||||
personId,
|
||||
});
|
||||
}
|
||||
|
||||
if (workspaceMemberId) {
|
||||
await this.matchParticipantService.matchParticipantsAfterWorkspaceMemberCreation(
|
||||
{
|
||||
handle: email,
|
||||
objectMetadataName: 'messageParticipant',
|
||||
workspaceMemberId,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,11 +28,11 @@ export class MessageParticipantUnmatchParticipantJob {
|
||||
): Promise<void> {
|
||||
const { email, personId, workspaceMemberId } = data;
|
||||
|
||||
await this.matchParticipantService.unmatchParticipants(
|
||||
email,
|
||||
'messageParticipant',
|
||||
await this.matchParticipantService.unmatchParticipants({
|
||||
handle: email,
|
||||
objectMetadataName: 'messageParticipant',
|
||||
personId,
|
||||
workspaceMemberId,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,14 +1,19 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator';
|
||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||
import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
|
||||
import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event';
|
||||
import { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event';
|
||||
import { objectRecordChangedProperties as objectRecordUpdateEventChangedProperties } from 'src/engine/core-modules/event-emitter/utils/object-record-changed-properties.util';
|
||||
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
||||
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type';
|
||||
import { computeChangedAdditionalEmails } from 'src/modules/contact-creation-manager/utils/compute-changed-additional-emails';
|
||||
import { hasPrimaryEmailChanged } from 'src/modules/contact-creation-manager/utils/has-primary-email-changed';
|
||||
import {
|
||||
MessageParticipantMatchParticipantJob,
|
||||
MessageParticipantMatchParticipantJobData,
|
||||
@ -33,18 +38,42 @@ export class MessageParticipantPersonListener {
|
||||
>,
|
||||
) {
|
||||
for (const eventPayload of payload.events) {
|
||||
if (!eventPayload.properties.after.emails?.primaryEmail) {
|
||||
continue;
|
||||
const jobPromises: Promise<void>[] = [];
|
||||
|
||||
if (isDefined(eventPayload.properties.after.emails?.primaryEmail)) {
|
||||
jobPromises.push(
|
||||
this.messageQueueService.add<MessageParticipantMatchParticipantJobData>(
|
||||
MessageParticipantMatchParticipantJob.name,
|
||||
{
|
||||
workspaceId: payload.workspaceId,
|
||||
email: eventPayload.properties.after.emails?.primaryEmail,
|
||||
isPrimaryEmail: true,
|
||||
personId: eventPayload.recordId,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await this.messageQueueService.add<MessageParticipantMatchParticipantJobData>(
|
||||
MessageParticipantMatchParticipantJob.name,
|
||||
{
|
||||
workspaceId: payload.workspaceId,
|
||||
email: eventPayload.properties.after.emails?.primaryEmail,
|
||||
personId: eventPayload.recordId,
|
||||
},
|
||||
);
|
||||
const additionalEmails =
|
||||
eventPayload.properties.after.emails?.additionalEmails;
|
||||
|
||||
if (Array.isArray(additionalEmails)) {
|
||||
const additionalEmailPromises = additionalEmails.map((email) =>
|
||||
this.messageQueueService.add<MessageParticipantMatchParticipantJobData>(
|
||||
MessageParticipantMatchParticipantJob.name,
|
||||
{
|
||||
workspaceId: payload.workspaceId,
|
||||
email: email,
|
||||
isPrimaryEmail: false,
|
||||
personId: eventPayload.recordId,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
jobPromises.push(...additionalEmailPromises);
|
||||
}
|
||||
|
||||
await Promise.all(jobPromises);
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,23 +90,106 @@ export class MessageParticipantPersonListener {
|
||||
eventPayload.properties.after,
|
||||
).includes('emails')
|
||||
) {
|
||||
await this.messageQueueService.add<MessageParticipantUnmatchParticipantJobData>(
|
||||
MessageParticipantUnmatchParticipantJob.name,
|
||||
{
|
||||
workspaceId: payload.workspaceId,
|
||||
email: eventPayload.properties.before.emails?.primaryEmail,
|
||||
personId: eventPayload.recordId,
|
||||
},
|
||||
if (!isDefined(eventPayload.properties.diff)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const jobPromises: Promise<void>[] = [];
|
||||
|
||||
if (hasPrimaryEmailChanged(eventPayload.properties.diff)) {
|
||||
if (eventPayload.properties.before.emails?.primaryEmail) {
|
||||
jobPromises.push(
|
||||
this.messageQueueService.add<MessageParticipantUnmatchParticipantJobData>(
|
||||
MessageParticipantUnmatchParticipantJob.name,
|
||||
{
|
||||
workspaceId: payload.workspaceId,
|
||||
email: eventPayload.properties.before.emails?.primaryEmail,
|
||||
personId: eventPayload.recordId,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (eventPayload.properties.after.emails?.primaryEmail) {
|
||||
jobPromises.push(
|
||||
this.messageQueueService.add<MessageParticipantMatchParticipantJobData>(
|
||||
MessageParticipantMatchParticipantJob.name,
|
||||
{
|
||||
workspaceId: payload.workspaceId,
|
||||
email: eventPayload.properties.after.emails?.primaryEmail,
|
||||
isPrimaryEmail: true,
|
||||
personId: eventPayload.recordId,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const { addedAdditionalEmails, removedAdditionalEmails } =
|
||||
computeChangedAdditionalEmails(eventPayload.properties.diff);
|
||||
|
||||
const removedEmailPromises = removedAdditionalEmails.map((email) =>
|
||||
this.messageQueueService.add<MessageParticipantUnmatchParticipantJobData>(
|
||||
MessageParticipantUnmatchParticipantJob.name,
|
||||
{
|
||||
workspaceId: payload.workspaceId,
|
||||
email: email,
|
||||
personId: eventPayload.recordId,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await this.messageQueueService.add<MessageParticipantMatchParticipantJobData>(
|
||||
MessageParticipantMatchParticipantJob.name,
|
||||
{
|
||||
workspaceId: payload.workspaceId,
|
||||
email: eventPayload.properties.after.emails?.primaryEmail,
|
||||
personId: eventPayload.recordId,
|
||||
},
|
||||
const addedEmailPromises = addedAdditionalEmails.map((email) =>
|
||||
this.messageQueueService.add<MessageParticipantMatchParticipantJobData>(
|
||||
MessageParticipantMatchParticipantJob.name,
|
||||
{
|
||||
workspaceId: payload.workspaceId,
|
||||
email: email,
|
||||
isPrimaryEmail: false,
|
||||
personId: eventPayload.recordId,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
jobPromises.push(...removedEmailPromises, ...addedEmailPromises);
|
||||
|
||||
await Promise.all(jobPromises);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OnDatabaseBatchEvent('person', DatabaseEventAction.DESTROYED)
|
||||
async handleDestroyedEvent(
|
||||
payload: WorkspaceEventBatch<
|
||||
ObjectRecordDeleteEvent<PersonWorkspaceEntity>
|
||||
>,
|
||||
) {
|
||||
for (const eventPayload of payload.events) {
|
||||
await this.messageQueueService.add<MessageParticipantUnmatchParticipantJobData>(
|
||||
MessageParticipantUnmatchParticipantJob.name,
|
||||
{
|
||||
workspaceId: payload.workspaceId,
|
||||
email: eventPayload.properties.before.emails?.primaryEmail,
|
||||
personId: eventPayload.recordId,
|
||||
},
|
||||
);
|
||||
|
||||
const additionalEmails =
|
||||
eventPayload.properties.before.emails?.additionalEmails;
|
||||
|
||||
if (Array.isArray(additionalEmails)) {
|
||||
const additionalEmailPromises = additionalEmails.map((email) =>
|
||||
this.messageQueueService.add<MessageParticipantUnmatchParticipantJobData>(
|
||||
MessageParticipantUnmatchParticipantJob.name,
|
||||
{
|
||||
workspaceId: payload.workspaceId,
|
||||
email: email,
|
||||
personId: eventPayload.recordId,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await Promise.all(additionalEmailPromises);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator';
|
||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||
@ -61,6 +61,7 @@ export class MessageParticipantWorkspaceMemberListener {
|
||||
workspaceId: payload.workspaceId,
|
||||
email: eventPayload.properties.after.userEmail,
|
||||
workspaceMemberId: eventPayload.recordId,
|
||||
isPrimaryEmail: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -94,6 +95,7 @@ export class MessageParticipantWorkspaceMemberListener {
|
||||
workspaceId: payload.workspaceId,
|
||||
email: eventPayload.properties.after.userEmail,
|
||||
workspaceMemberId: eventPayload.recordId,
|
||||
isPrimaryEmail: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -35,10 +35,10 @@ export class MessagingMessageParticipantService {
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
await this.matchParticipantService.matchParticipants(
|
||||
savedParticipants,
|
||||
'messageParticipant',
|
||||
await this.matchParticipantService.matchParticipants({
|
||||
participants: savedParticipants,
|
||||
objectMetadataName: 'messageParticipant',
|
||||
transactionManager,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user