Skip to content

Commit

Permalink
feat: Adds contacts.checkExistence endpoint (#34194)
Browse files Browse the repository at this point in the history
  • Loading branch information
matheusbsilva137 authored Dec 19, 2024
1 parent 82e667f commit b845fc0
Show file tree
Hide file tree
Showing 6 changed files with 243 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .changeset/big-timers-relax.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@rocket.chat/rest-typings': minor
'@rocket.chat/meteor': minor
---

Adds a new `contacts.checkExistence` endpoint, which allows identifying whether there's already a registered contact using a given email, phone, id or visitor to source association.
15 changes: 15 additions & 0 deletions apps/meteor/app/livechat/server/api/v1/contact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
isGETOmnichannelContactHistoryProps,
isGETOmnichannelContactsChannelsProps,
isGETOmnichannelContactsSearchProps,
isGETOmnichannelContactsCheckExistenceProps,
} from '@rocket.chat/rest-typings';
import { escapeRegExp } from '@rocket.chat/string-helpers';
import { Match, check } from 'meteor/check';
Expand Down Expand Up @@ -166,6 +167,20 @@ API.v1.addRoute(
},
);

API.v1.addRoute(
'omnichannel/contacts.checkExistence',
{ authRequired: true, permissionsRequired: ['view-livechat-contact'], validateParams: isGETOmnichannelContactsCheckExistenceProps },
{
async get() {
const { contactId, visitor, email, phone } = this.queryParams;

const contact = await (visitor ? getContactByChannel(visitor) : LivechatContacts.countByContactInfo({ contactId, email, phone }));

return API.v1.success({ exists: !!contact });
},
},
);

API.v1.addRoute(
'omnichannel/contacts.history',
{ authRequired: true, permissionsRequired: ['view-livechat-contact-history'], validateParams: isGETOmnichannelContactHistoryProps },
Expand Down
10 changes: 10 additions & 0 deletions apps/meteor/server/models/raw/LivechatContacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,4 +262,14 @@ export class LivechatContactsRaw extends BaseRaw<ILivechatContact> implements IL

return updatedContact.value;
}

countByContactInfo({ contactId, email, phone }: { contactId?: string; email?: string; phone?: string }): Promise<number> {
const filter = {
...(email && { 'emails.address': email }),
...(phone && { 'phones.phoneNumber': phone }),
...(contactId && { _id: contactId }),
};

return this.countDocuments(filter);
}
}
147 changes: 147 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 @@ -1061,6 +1061,153 @@ describe('LIVECHAT - contacts', () => {
});
});

describe('[GET] omnichannel/contacts.checkExistence', () => {
let contactId: string;
let association: ILivechatContactVisitorAssociation;
let roomId: string;

const email = faker.internet.email().toLowerCase();
const phone = faker.phone.number();

const contact = {
name: faker.person.fullName(),
emails: [email],
phones: [phone],
contactManager: agentUser?._id,
};

before(async () => {
await updatePermission('view-livechat-contact', ['admin']);
const { body } = await request
.post(api('omnichannel/contacts'))
.set(credentials)
.send({ ...contact });
contactId = body.contactId;

const visitor = await createVisitor(undefined, contact.name, email, phone);

const room = await createLivechatRoom(visitor.token);
roomId = room._id;
association = {
visitorId: visitor._id,
source: {
type: room.source.type,
id: room.source.id,
},
};
});

after(async () => Promise.all([restorePermissionToRoles('view-livechat-contact'), closeOmnichannelRoom(roomId)]));

it('should confirm a contact exists when checking by contact id', async () => {
const res = await request.get(api(`omnichannel/contacts.checkExistence`)).set(credentials).query({ contactId });

expect(res.status).to.be.equal(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('exists', true);
});

it('should confirm a contact does not exist when checking by contact id', async () => {
const res = await request.get(api(`omnichannel/contacts.checkExistence`)).set(credentials).query({ contactId: 'invalid-contact-id' });

expect(res.status).to.be.equal(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('exists', false);
});

it('should confirm a contact exists when checking by visitor association', async () => {
const res = await request.get(api(`omnichannel/contacts.checkExistence`)).set(credentials).query({ visitor: association });

expect(res.status).to.be.equal(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('exists', true);
});

it('should confirm a contact does not exist when checking by visitor association', async () => {
const res = await request
.get(api(`omnichannel/contacts.checkExistence`))
.set(credentials)
.query({ visitor: { ...association, visitorId: 'invalid-id' } });

expect(res.status).to.be.equal(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('exists', false);
});

it('should confirm a contact exists when checking by email', async () => {
const res = await request.get(api(`omnichannel/contacts.checkExistence`)).set(credentials).query({ email });

expect(res.status).to.be.equal(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('exists', true);
});

it('should confirm a contact does not exist when checking by email', async () => {
const res = await request
.get(api(`omnichannel/contacts.checkExistence`))
.set(credentials)
.query({ email: 'invalid-email@example.com' });

expect(res.status).to.be.equal(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('exists', false);
});

it('should confirm a contact exists when checking by phone', async () => {
const res = await request.get(api(`omnichannel/contacts.checkExistence`)).set(credentials).query({ phone });

expect(res.status).to.be.equal(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('exists', true);
});

it('should confirm a contact does not exist when checking by phone', async () => {
const res = await request.get(api(`omnichannel/contacts.checkExistence`)).set(credentials).query({ phone: 'invalid-phone' });

expect(res.status).to.be.equal(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('exists', false);
});

it("should return an error if user doesn't have 'view-livechat-contact' permission", async () => {
await removePermissionFromAllRoles('view-livechat-contact');

const res = await request.get(api(`omnichannel/contacts.checkExistence`)).set(credentials).query({ contactId });

expect(res.body).to.have.property('success', false);
expect(res.body.error).to.be.equal('User does not have the permissions required for this action [error-unauthorized]');

await restorePermissionToRoles('view-livechat-contact');
});

it('should return an error if all query params are missing', async () => {
const res = await request.get(api(`omnichannel/contacts.checkExistence`)).set(credentials);

expectInvalidParams(res, [
"must have required property 'contactId'",
"must have required property 'email'",
"must have required property 'phone'",
"must have required property 'visitor'",
'must match exactly one schema in oneOf [invalid-params]',
]);
});

it('should return an error if more than one field is provided', async () => {
const res = await request
.get(api(`omnichannel/contacts.checkExistence`))
.set(credentials)
.query({ contactId, visitor: association, email, phone });

expectInvalidParams(res, [
'must NOT have additional properties',
'must NOT have additional properties',
'must NOT have additional properties',
'must NOT have additional properties',
'must match exactly one schema in oneOf [invalid-params]',
]);
});
});

describe('[GET] omnichannel/contacts.search', () => {
let contactId: string;
let visitor: ILivechatVisitor;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,5 @@ export interface ILivechatContactsModel extends IBaseModel<ILivechatContact> {
setChannelVerifiedStatus(visitor: ILivechatContactVisitorAssociation, verified: boolean): Promise<UpdateResult>;
setVerifiedUpdateQuery(verified: boolean, contactUpdater: Updater<ILivechatContact>): Updater<ILivechatContact>;
setFieldAndValueUpdateQuery(field: string, value: string, contactUpdater: Updater<ILivechatContact>): Updater<ILivechatContact>;
countByContactInfo({ contactId, email, phone }: { contactId?: string; email?: string; phone?: string }): Promise<number>;
}
64 changes: 64 additions & 0 deletions packages/rest-typings/src/v1/omnichannel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1418,6 +1418,67 @@ const GETOmnichannelContactsSearchSchema = {

export const isGETOmnichannelContactsSearchProps = ajv.compile<GETOmnichannelContactsSearchProps>(GETOmnichannelContactsSearchSchema);

type GETOmnichannelContactsCheckExistenceProps = {
contactId?: string;
email?: string;
phone?: string;
visitor?: ILivechatContactVisitorAssociation;
};

const GETOmnichannelContactsCheckExistenceSchema = {
oneOf: [
{
type: 'object',
properties: {
contactId: {
type: 'string',
nullable: false,
isNotEmpty: true,
},
},
required: ['contactId'],
additionalProperties: false,
},
{
type: 'object',
properties: {
email: {
type: 'string',
format: 'basic_email',
nullable: false,
isNotEmpty: true,
},
},
required: ['email'],
additionalProperties: false,
},
{
type: 'object',
properties: {
phone: {
type: 'string',
nullable: false,
isNotEmpty: true,
},
},
required: ['phone'],
additionalProperties: false,
},
{
type: 'object',
properties: {
visitor: ContactVisitorAssociationSchema,
},
required: ['visitor'],
additionalProperties: false,
},
],
};

export const isGETOmnichannelContactsCheckExistenceProps = ajv.compile<GETOmnichannelContactsCheckExistenceProps>(
GETOmnichannelContactsCheckExistenceSchema,
);

type GETOmnichannelContactHistoryProps = PaginatedRequest<{
contactId: string;
source?: string;
Expand Down Expand Up @@ -3867,6 +3928,9 @@ export type OmnichannelEndpoints = {
'/v1/omnichannel/contacts.search': {
GET: (params: GETOmnichannelContactsSearchProps) => PaginatedResult<{ contacts: ILivechatContactWithManagerData[] }>;
};
'/v1/omnichannel/contacts.checkExistence': {
GET: (params: GETOmnichannelContactsCheckExistenceProps) => { exists: boolean };
};
'/v1/omnichannel/contacts.history': {
GET: (params: GETOmnichannelContactHistoryProps) => PaginatedResult<{ history: ContactSearchChatsResult[] }>;
};
Expand Down

0 comments on commit b845fc0

Please sign in to comment.