Skip to content

Commit

Permalink
feat: use contact's name on as omnichannel rooms' display name (#33893)
Browse files Browse the repository at this point in the history
* feat: use contact's name on as omnichannel rooms' display name

* added some tests

* tests

* fix tests
  • Loading branch information
pierre-lehnen-rc authored Nov 6, 2024
1 parent 082908b commit 118232e
Show file tree
Hide file tree
Showing 11 changed files with 148 additions and 38 deletions.
11 changes: 8 additions & 3 deletions apps/meteor/app/livechat/server/lib/Helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
IOmnichannelRoomInfo,
IOmnichannelInquiryExtraData,
IOmnichannelRoomExtraData,
ILivechatContact,
} from '@rocket.chat/core-typings';
import { LivechatInquiryStatus, OmnichannelSourceType, DEFAULT_SLA_CONFIG, UserStatus } from '@rocket.chat/core-typings';
import { LivechatPriorityWeight } from '@rocket.chat/core-typings/src/ILivechatPriority';
Expand All @@ -29,6 +30,7 @@ import {
Subscriptions,
Rooms,
Users,
LivechatContacts,
} from '@rocket.chat/models';
import { Match, check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
Expand Down Expand Up @@ -66,13 +68,11 @@ export const allowAgentSkipQueue = (agent: SelectedAgent) => {
};
export const createLivechatRoom = async (
rid: string,
name: string,
guest: ILivechatVisitor,
roomInfo: IOmnichannelRoomInfo = { source: { type: OmnichannelSourceType.OTHER } },
extraData?: IOmnichannelRoomExtraData,
) => {
check(rid, String);
check(name, String);
check(
guest,
Match.ObjectIncluding({
Expand All @@ -94,14 +94,19 @@ export const createLivechatRoom = async (
});

const contactId = await migrateVisitorIfMissingContact(_id, extraRoomInfo.source || roomInfo.source);
const contact =
contactId && (await LivechatContacts.findOneById<Pick<ILivechatContact, '_id' | 'name'>>(contactId, { projection: { name: 1 } }));
if (!contact) {
throw new Error('error-invalid-contact');
}

// TODO: Solve `u` missing issue
const room: InsertionModel<IOmnichannelRoom> = {
_id: rid,
msgs: 0,
usersCount: 1,
lm: newRoomAt,
fname: name,
fname: contact.name,
t: 'l' as const,
ts: newRoomAt,
departmentId,
Expand Down
6 changes: 2 additions & 4 deletions apps/meteor/app/livechat/server/lib/QueueManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,9 +214,7 @@ export class QueueManager {
}
}

const name = guest.name || guest.username;

const room = await createLivechatRoom(rid, name, { ...guest, ...(department && { department }) }, roomInfo, {
const room = await createLivechatRoom(rid, { ...guest, ...(department && { department }) }, roomInfo, {
...extraData,
...(Boolean(customFields) && { customFields }),
});
Expand All @@ -229,7 +227,7 @@ export class QueueManager {

const inquiry = await createLivechatInquiry({
rid,
name,
name: room.fname,
initialStatus: await this.getInquiryStatus({ room, agent: defaultAgent }),
guest,
message,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,16 @@ export async function createContactFromVisitor(

const contactId = await createContact(contactData, useVisitorId ? visitor._id : undefined);

await LivechatRooms.setContactIdByVisitorAssociation(contactId, {
visitorId: visitor._id,
source: { type: source.type, ...(source.id ? { id: source.id } : {}) },
});
await LivechatRooms.setContactByVisitorAssociation(
{
visitorId: visitor._id,
source: { type: source.type, ...(source.id ? { id: source.id } : {}) },
},
{
_id: contactId,
name: contactData.name,
},
);

return contactId;
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,13 @@ export async function migrateVisitorToContactId(
await ContactMerger.mergeVisitorIntoContact(visitor, existingContact, source);

// Update all existing rooms matching the visitor id and source to set the contactId to them
await LivechatRooms.setContactIdByVisitorAssociation(existingContact._id, {
visitorId: visitor._id,
source,
});
await LivechatRooms.setContactByVisitorAssociation(
{
visitorId: visitor._id,
source,
},
existingContact,
);

return existingContact._id;
}
11 changes: 9 additions & 2 deletions apps/meteor/app/livechat/server/lib/contacts/updateContact.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ILivechatContact, ILivechatContactChannel } from '@rocket.chat/core-typings';
import { LivechatContacts } from '@rocket.chat/models';
import { LivechatContacts, LivechatRooms } from '@rocket.chat/models';

import { getAllowedCustomFields } from './getAllowedCustomFields';
import { validateContactManager } from './validateContactManager';
Expand All @@ -19,7 +19,9 @@ export type UpdateContactParams = {
export async function updateContact(params: UpdateContactParams): Promise<ILivechatContact> {
const { contactId, name, emails, phones, customFields: receivedCustomFields, contactManager, channels, wipeConflicts } = params;

const contact = await LivechatContacts.findOneById<Pick<ILivechatContact, '_id'>>(contactId, { projection: { _id: 1 } });
const contact = await LivechatContacts.findOneById<Pick<ILivechatContact, '_id' | 'name'>>(contactId, {
projection: { _id: 1, name: 1 },
});

if (!contact) {
throw new Error('error-contact-not-found');
Expand All @@ -41,5 +43,10 @@ export async function updateContact(params: UpdateContactParams): Promise<ILivec
...(wipeConflicts && { conflictingFields: [] }),
});

// If the contact name changed, update the name of its existing rooms
if (name !== undefined && name !== contact.name) {
await LivechatRooms.updateContactDataByContactId(contactId, { name });
}

return updatedContact;
}
11 changes: 0 additions & 11 deletions apps/meteor/ee/server/models/raw/LivechatRooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ declare module '@rocket.chat/model-typings' {
getConversationsWithoutTagsBetweenDate(start: Date, end: Date, extraQuery: Filter<IOmnichannelRoom>): Promise<number>;
getTotalConversationsWithoutAgentsBetweenDate(start: Date, end: Date, extraQuery: Filter<IOmnichannelRoom>): Promise<number>;
getTotalConversationsWithoutDepartmentBetweenDates(start: Date, end: Date, extraQuery: Filter<IOmnichannelRoom>): Promise<number>;
replaceContactId(oldContactId: string, newContactId: string): Promise<UpdateResult | Document>;
}
}

Expand Down Expand Up @@ -727,14 +726,4 @@ export class LivechatRoomsRawEE extends LivechatRoomsRaw implements ILivechatRoo
...extraQuery,
});
}

replaceContactId(oldContactId: string, newContactId: string): Promise<UpdateResult | Document> {
return this.updateMany(
{
't': 'l',
'v.contactId': oldContactId,
},
{ $set: { 'v.contactId': newContactId } },
);
}
}
44 changes: 37 additions & 7 deletions apps/meteor/server/models/raw/LivechatRooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import type {
ReportResult,
MACStats,
ILivechatContactVisitorAssociation,
ILivechatContact,
AtLeast,
} from '@rocket.chat/core-typings';
import { UserStatus } from '@rocket.chat/core-typings';
import type { FindPaginated, ILivechatRoomsModel } from '@rocket.chat/model-typings';
Expand Down Expand Up @@ -2761,19 +2763,47 @@ export class LivechatRoomsRaw extends BaseRaw<IOmnichannelRoom> implements ILive
throw new Error('Method not implemented.');
}

setContactIdByVisitorAssociation(contactId: string, visitor: ILivechatContactVisitorAssociation): Promise<UpdateResult | Document> {
setContactByVisitorAssociation(
association: ILivechatContactVisitorAssociation,
contact: Pick<AtLeast<ILivechatContact, '_id'>, '_id' | 'name'>,
): Promise<UpdateResult | Document> {
return this.updateMany(
{
't': 'l',
'v._id': visitor.visitorId,
'source.type': visitor.source.type,
...(visitor.source.id ? { 'source.id': visitor.source.id } : {}),
'v._id': association.visitorId,
'source.type': association.source.type,
...(association.source.id ? { 'source.id': association.source.id } : {}),
},
{
$set: {
'v.contactId': contact._id,
...(contact.name ? { fname: contact.name } : {}),
},
},
{ $set: { 'v.contactId': contactId } },
);
}

replaceContactId(_oldContactId: string, _newContactId: string): Promise<UpdateResult | Document> {
throw new Error('Method not implemented.');
updateContactDataByContactId(
oldContactId: ILivechatContact['_id'],
contact: Partial<Pick<ILivechatContact, '_id' | 'name'>>,
): Promise<UpdateResult | Document> {
const update = {
...(contact._id ? { 'v.contactId': contact._id } : {}),
...(contact.name ? { fname: contact.name } : {}),
};

if (!Object.keys(update).length) {
throw new Error('error-invalid-operation');
}

return this.updateMany(
{
't': 'l',
'v.contactId': oldContactId,
},
{
$set: update,
},
);
}
}
59 changes: 59 additions & 0 deletions apps/meteor/tests/end-to-end/api/livechat/contacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,65 @@ describe('LIVECHAT - contacts', () => {
});
});

describe('Contact Rooms', () => {
before(async () => {
await updatePermission('view-livechat-contact', ['admin']);
});

after(async () => {
await restorePermissionToRoles('view-livechat-contact');
});

it('should create a contact and assign it to the room', async () => {
const visitor = await createVisitor();
const room = await createLivechatRoom(visitor.token);
expect(room.v).to.have.property('contactId').that.is.a('string');
});

it('should create a room using the pre-created contact', async () => {
const email = faker.internet.email().toLowerCase();
const phone = faker.phone.number();

const contact = {
name: 'Contact Name',
emails: [email],
phones: [phone],
contactManager: agentUser?._id,
};

const { body } = await request
.post(api('omnichannel/contacts'))
.set(credentials)
.send({ ...contact });
const { contactId } = body;

const visitor = await createVisitor(undefined, 'Visitor Name', email, phone);

const room = await createLivechatRoom(visitor.token);

expect(room.v).to.have.property('contactId', contactId);
expect(room).to.have.property('fname', 'Contact Name');
});

it('should update room names when a contact name changes', async () => {
const visitor = await createVisitor();
const room = await createLivechatRoom(visitor.token);
expect(room.v).to.have.property('contactId').that.is.a('string');
expect(room.fname).to.not.be.equal('New Contact Name');

const res = await request.post(api('omnichannel/contacts.update')).set(credentials).send({
contactId: room.v.contactId,
name: 'New Contact Name',
});

expect(res.status).to.be.equal(200);

const sameRoom = await createLivechatRoom(visitor.token, { rid: room._id });
expect(sameRoom._id).to.be.equal(room._id);
expect(sameRoom.fname).to.be.equal('New Contact Name');
});
});

describe('[GET] omnichannel/contacts.get', () => {
let contactId: string;
let contactId2: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const modelsMock = {
findContactMatchingVisitor: sinon.stub(),
},
LivechatRooms: {
setContactIdByVisitorAssociation: sinon.stub(),
setContactByVisitorAssociation: sinon.stub(),
findNewestByContactVisitorAssociation: sinon.stub(),
},
};
Expand Down Expand Up @@ -39,7 +39,7 @@ const { migrateVisitorToContactId } = proxyquire
describe('migrateVisitorToContactId', () => {
beforeEach(() => {
modelsMock.LivechatContacts.findContactMatchingVisitor.reset();
modelsMock.LivechatRooms.setContactIdByVisitorAssociation.reset();
modelsMock.LivechatRooms.setContactByVisitorAssociation.reset();
modelsMock.LivechatRooms.findNewestByContactVisitorAssociation.reset();
createContactFromVisitor.reset();
mergeVisitorIntoContact.reset();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ const modelsMock = {
findOneById: sinon.stub(),
updateContact: sinon.stub(),
},
LivechatRooms: {
updateContactDataByContactId: sinon.stub(),
},
};

const { updateContact } = proxyquire.noCallThru().load('../../../../../../app/livechat/server/lib/contacts/updateContact', {
Expand All @@ -27,6 +30,7 @@ describe('updateContact', () => {
beforeEach(() => {
modelsMock.LivechatContacts.findOneById.reset();
modelsMock.LivechatContacts.updateContact.reset();
modelsMock.LivechatRooms.updateContactDataByContactId.reset();
});

it('should throw an error if the contact does not exist', async () => {
Expand Down
11 changes: 10 additions & 1 deletion packages/model-typings/src/models/ILivechatRoomsModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import type {
ILivechatVisitor,
MACStats,
ILivechatContactVisitorAssociation,
AtLeast,
ILivechatContact,
} from '@rocket.chat/core-typings';
import type { FindCursor, UpdateResult, AggregationCursor, Document, FindOptions, DeleteResult, Filter, UpdateOptions } from 'mongodb';

Expand Down Expand Up @@ -269,11 +271,18 @@ export interface ILivechatRoomsModel extends IBaseModel<IOmnichannelRoom> {
association: ILivechatContactVisitorAssociation,
options?: Omit<FindOptions<IOmnichannelRoom>, 'sort' | 'limit'>,
): Promise<T | null>;
setContactIdByVisitorAssociation(contactId: string, visitor: ILivechatContactVisitorAssociation): Promise<UpdateResult | Document>;
setContactByVisitorAssociation(
association: ILivechatContactVisitorAssociation,
contact: Pick<AtLeast<ILivechatContact, '_id'>, '_id' | 'name'>,
): Promise<UpdateResult | Document>;
findClosedRoomsByContactAndSourcePaginated(params: {
contactId: string;
source?: string;
options?: FindOptions;
}): FindPaginated<FindCursor<IOmnichannelRoom>>;
countLivechatRoomsWithDepartment(): Promise<number>;
updateContactDataByContactId(
oldContactId: ILivechatContact['_id'],
contact: Partial<Pick<ILivechatContact, '_id' | 'name'>>,
): Promise<UpdateResult | Document>;
}

0 comments on commit 118232e

Please sign in to comment.