Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Adds contacts.checkExistence endpoint #34194

Merged
merged 10 commits into from
Dec 19, 2024
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.
20 changes: 20 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,25 @@ 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 filter = {
matheusbsilva137 marked this conversation as resolved.
Show resolved Hide resolved
...(email && { 'emails.address': email }),
...(phone && { 'phones.phoneNumber': phone }),
...(contactId && { _id: contactId }),
};

const contact = await (visitor ? getContactByChannel(visitor) : LivechatContacts.countDocuments(filter));
KevLehman marked this conversation as resolved.
Show resolved Hide resolved

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

API.v1.addRoute(
'omnichannel/contacts.history',
{ authRequired: true, permissionsRequired: ['view-livechat-contact-history'], validateParams: isGETOmnichannelContactHistoryProps },
Expand Down
144 changes: 144 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 @@ -950,6 +950,150 @@ describe('LIVECHAT - contacts', () => {
});
});

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

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
MarcosSpessatto marked this conversation as resolved.
Show resolved Hide resolved
.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);
matheusbsilva137 marked this conversation as resolved.
Show resolved Hide resolved
association = {
visitorId: visitor._id,
source: {
type: room.source.type,
id: room.source.id,
},
};
});

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

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' });

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
63 changes: 63 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,66 @@ 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',
matheusbsilva137 marked this conversation as resolved.
Show resolved Hide resolved
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 +3927,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
Loading