fix: GDPR for contacts (#36228)

pull/36396/head^2
Kevin Aleman 6 months ago committed by GitHub
parent b0be3e5664
commit 68426bdfae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 7
      .changeset/tough-points-know.md
  2. 2
      apps/meteor/app/apps/server/converters/contacts.ts
  3. 2
      apps/meteor/app/apps/server/converters/rooms.js
  4. 2
      apps/meteor/app/livechat/server/api/v1/contact.ts
  5. 24
      apps/meteor/app/livechat/server/api/v1/visitor.ts
  6. 2
      apps/meteor/app/livechat/server/lib/Helper.ts
  7. 2
      apps/meteor/app/livechat/server/lib/QueueManager.ts
  8. 2
      apps/meteor/app/livechat/server/lib/contacts/getContactChannelsGrouped.ts
  9. 2
      apps/meteor/app/livechat/server/lib/contacts/getContactHistory.ts
  10. 30
      apps/meteor/app/livechat/server/lib/contacts/resolveContactConflicts.spec.ts
  11. 9
      apps/meteor/app/livechat/server/lib/contacts/resolveContactConflicts.ts
  12. 8
      apps/meteor/app/livechat/server/lib/contacts/updateContact.spec.ts
  13. 2
      apps/meteor/app/livechat/server/lib/contacts/updateContact.ts
  14. 33
      apps/meteor/app/livechat/server/lib/guests.ts
  15. 2
      apps/meteor/ee/app/livechat-enterprise/server/hooks/handleNextAgentPreferredEvents.ts
  16. 2
      apps/meteor/ee/server/patches/isAgentAvailableToTakeContactInquiry.ts
  17. 4
      apps/meteor/ee/server/patches/mergeContacts.ts
  18. 22
      apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/lib/isAgentAvailableToTakeContactInquiry.spec.ts
  19. 18
      apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/lib/mergeContacts.spec.ts
  20. 26
      apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts
  21. 4
      packages/model-typings/src/models/ILivechatContactsModel.ts
  22. 1
      packages/model-typings/src/models/ILivechatInquiryModel.ts
  23. 1
      packages/model-typings/src/models/ILivechatRoomsModel.ts
  24. 34
      packages/models/src/models/LivechatContacts.ts
  25. 4
      packages/models/src/models/LivechatInquiry.ts
  26. 9
      packages/models/src/models/LivechatRooms.ts

@ -0,0 +1,7 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/model-typings": patch
"@rocket.chat/models": patch
---
Fixes GDPR contact information removal for Omnichannel.

@ -6,7 +6,7 @@ import { transformMappedData } from './transformMappedData';
export class AppContactsConverter implements IAppContactsConverter {
async convertById(contactId: ILivechatContact['_id']): Promise<IAppsLivechatContact | undefined> {
const contact = await LivechatContacts.findOneById(contactId);
const contact = await LivechatContacts.findOneEnabledById(contactId);
if (!contact) {
return;
}

@ -105,7 +105,7 @@ export class AppRoomsConverter {
if (!contact?._id) {
return;
}
const contactFromDb = await LivechatContacts.findOneById(contact._id, { projection: { _id: 1 } });
const contactFromDb = await LivechatContacts.findOneEnabledById(contact._id, { projection: { _id: 1 } });
return contactFromDb?._id;
}

@ -154,7 +154,7 @@ API.v1.addRoute(
return API.v1.notFound();
}
const contact = await LivechatContacts.findOneById(contactId);
const contact = await LivechatContacts.findOneEnabledById(contactId);
if (!contact) {
return API.v1.notFound();

@ -7,7 +7,8 @@ import { callbacks } from '../../../../../lib/callbacks';
import { API } from '../../../../api/server';
import { settings } from '../../../../settings/server';
import { setMultipleVisitorCustomFields } from '../../lib/custom-fields';
import { registerGuest, removeGuest, notifyGuestStatusChanged } from '../../lib/guests';
import { registerGuest, notifyGuestStatusChanged, removeContactsByVisitorId } from '../../lib/guests';
import { livechatLogger } from '../../lib/logger';
import { saveRoomInfo } from '../../lib/rooms';
import { updateCallStatus } from '../../lib/utils';
import { findGuest, normalizeHttpHeaderData } from '../lib/livechat';
@ -140,18 +141,19 @@ API.v1.addRoute('livechat/visitor/:token', {
throw new Meteor.Error('visitor-has-open-rooms', 'Cannot remove visitors with opened rooms');
}
const { _id, token } = visitor;
const result = await removeGuest({ _id, token });
if (!result.modifiedCount) {
const { _id } = visitor;
try {
await removeContactsByVisitorId({ _id });
return API.v1.success({
visitor: {
_id,
ts: new Date().toISOString(),
},
});
} catch (e) {
livechatLogger.error(e);
throw new Meteor.Error('error-removing-visitor', 'An error ocurred while deleting visitor');
}
return API.v1.success({
visitor: {
_id,
ts: new Date().toISOString(),
},
});
},
});

@ -98,7 +98,7 @@ export const prepareLivechatRoom = async (
const contactId = await migrateVisitorIfMissingContact(_id, source);
const contact =
contactId &&
(await LivechatContacts.findOneById<Pick<ILivechatContact, '_id' | 'name' | 'channels' | 'activity'>>(contactId, {
(await LivechatContacts.findOneEnabledById<Pick<ILivechatContact, '_id' | 'name' | 'channels' | 'activity'>>(contactId, {
projection: { name: 1, channels: 1, activity: 1 },
}));
if (!contact) {

@ -219,7 +219,7 @@ export class QueueManager {
return false;
}
const contact = await LivechatContacts.findOneById(room.contactId, { projection: { channels: 1 } });
const contact = await LivechatContacts.findOneEnabledById(room.contactId, { projection: { channels: 1 } });
if (!contact) {
return false;
}

@ -2,7 +2,7 @@ import type { ILivechatContact, ILivechatContactChannel } from '@rocket.chat/cor
import { LivechatContacts } from '@rocket.chat/models';
export async function getContactChannelsGrouped(contactId: string): Promise<ILivechatContactChannel[]> {
const contact = await LivechatContacts.findOneById<Pick<ILivechatContact, 'channels'>>(contactId, { projection: { channels: 1 } });
const contact = await LivechatContacts.findOneEnabledById<Pick<ILivechatContact, 'channels'>>(contactId, { projection: { channels: 1 } });
if (!contact?.channels) {
return [];

@ -32,7 +32,7 @@ export const getContactHistory = makeFunction(
async (params: GetContactHistoryParams): Promise<PaginatedResult<{ history: VisitorSearchChatsResult[] }>> => {
const { contactId, count, offset, sort } = params;
const contact = await LivechatContacts.findOneById<Pick<ILivechatContact, '_id'>>(contactId, { projection: { _id: 1 } });
const contact = await LivechatContacts.findOneEnabledById<Pick<ILivechatContact, '_id'>>(contactId, { projection: { _id: 1 } });
if (!contact) {
throw new Error('error-contact-not-found');

@ -5,7 +5,7 @@ import sinon from 'sinon';
const modelsMock = {
LivechatContacts: {
findOneById: sinon.stub(),
findOneEnabledById: sinon.stub(),
updateContact: sinon.stub(),
},
Settings: {
@ -24,13 +24,13 @@ const { resolveContactConflicts } = proxyquire.noCallThru().load('./resolveConta
describe('resolveContactConflicts', () => {
beforeEach(() => {
modelsMock.LivechatContacts.findOneById.reset();
modelsMock.LivechatContacts.findOneEnabledById.reset();
modelsMock.Settings.incrementValueById.reset();
modelsMock.LivechatContacts.updateContact.reset();
});
it('should update the contact with the resolved custom field', async () => {
modelsMock.LivechatContacts.findOneById.resolves({
modelsMock.LivechatContacts.findOneEnabledById.resolves({
_id: 'contactId',
customFields: { customField: 'newValue' },
conflictingFields: [{ field: 'customFields.customField', value: 'oldValue' }],
@ -44,7 +44,7 @@ describe('resolveContactConflicts', () => {
const result = await resolveContactConflicts({ contactId: 'contactId', customField: { customField: 'newValue' } });
expect(modelsMock.LivechatContacts.findOneById.getCall(0).args[0]).to.be.equal('contactId');
expect(modelsMock.LivechatContacts.findOneEnabledById.getCall(0).args[0]).to.be.equal('contactId');
expect(modelsMock.Settings.incrementValueById.getCall(0).args[0]).to.be.equal('Livechat_conflicting_fields_counter');
expect(modelsMock.Settings.incrementValueById.getCall(0).args[1]).to.be.equal(1);
@ -59,7 +59,7 @@ describe('resolveContactConflicts', () => {
});
it('should update the contact with the resolved name', async () => {
modelsMock.LivechatContacts.findOneById.resolves({
modelsMock.LivechatContacts.findOneEnabledById.resolves({
_id: 'contactId',
name: 'Old Name',
customFields: { customField: 'newValue' },
@ -75,7 +75,7 @@ describe('resolveContactConflicts', () => {
const result = await resolveContactConflicts({ contactId: 'contactId', name: 'New Name' });
expect(modelsMock.LivechatContacts.findOneById.getCall(0).args[0]).to.be.equal('contactId');
expect(modelsMock.LivechatContacts.findOneEnabledById.getCall(0).args[0]).to.be.equal('contactId');
expect(modelsMock.Settings.incrementValueById.getCall(0).args[0]).to.be.equal('Livechat_conflicting_fields_counter');
expect(modelsMock.Settings.incrementValueById.getCall(0).args[1]).to.be.equal(1);
@ -91,7 +91,7 @@ describe('resolveContactConflicts', () => {
});
it('should update the contact with the resolved contact manager', async () => {
modelsMock.LivechatContacts.findOneById.resolves({
modelsMock.LivechatContacts.findOneEnabledById.resolves({
_id: 'contactId',
name: 'Name',
contactManager: 'contactManagerId',
@ -109,7 +109,7 @@ describe('resolveContactConflicts', () => {
const result = await resolveContactConflicts({ contactId: 'contactId', name: 'New Name' });
expect(modelsMock.LivechatContacts.findOneById.getCall(0).args[0]).to.be.equal('contactId');
expect(modelsMock.LivechatContacts.findOneEnabledById.getCall(0).args[0]).to.be.equal('contactId');
expect(modelsMock.Settings.incrementValueById.getCall(0).args[0]).to.be.equal('Livechat_conflicting_fields_counter');
expect(modelsMock.Settings.incrementValueById.getCall(0).args[1]).to.be.equal(1);
@ -126,7 +126,7 @@ describe('resolveContactConflicts', () => {
it('should wipe conflicts if wipeConflicts = true', async () => {
it('should update the contact with the resolved name', async () => {
modelsMock.LivechatContacts.findOneById.resolves({
modelsMock.LivechatContacts.findOneEnabledById.resolves({
_id: 'contactId',
name: 'Name',
customFields: { customField: 'newValue' },
@ -145,7 +145,7 @@ describe('resolveContactConflicts', () => {
const result = await resolveContactConflicts({ contactId: 'contactId', name: 'New Name', wipeConflicts: true });
expect(modelsMock.LivechatContacts.findOneById.getCall(0).args[0]).to.be.equal('contactId');
expect(modelsMock.LivechatContacts.findOneEnabledById.getCall(0).args[0]).to.be.equal('contactId');
expect(modelsMock.Settings.incrementValueById.getCall(0).args[0]).to.be.equal('Livechat_conflicting_fields_counter');
expect(modelsMock.Settings.incrementValueById.getCall(0).args[1]).to.be.equal(2);
@ -163,7 +163,7 @@ describe('resolveContactConflicts', () => {
it('should wipe conflicts if wipeConflicts = true', async () => {
it('should update the contact with the resolved name', async () => {
modelsMock.LivechatContacts.findOneById.resolves({
modelsMock.LivechatContacts.findOneEnabledById.resolves({
_id: 'contactId',
name: 'Name',
customFields: { customField: 'newValue' },
@ -182,7 +182,7 @@ describe('resolveContactConflicts', () => {
const result = await resolveContactConflicts({ contactId: 'contactId', name: 'New Name', wipeConflicts: false });
expect(modelsMock.LivechatContacts.findOneById.getCall(0).args[0]).to.be.equal('contactId');
expect(modelsMock.LivechatContacts.findOneEnabledById.getCall(0).args[0]).to.be.equal('contactId');
expect(modelsMock.Settings.incrementValueById.getCall(0).args[0]).to.be.equal('Livechat_conflicting_fields_counter');
expect(modelsMock.Settings.incrementValueById.getCall(0).args[1]).to.be.equal(1);
@ -199,7 +199,7 @@ describe('resolveContactConflicts', () => {
});
it('should throw an error if the contact does not exist', async () => {
modelsMock.LivechatContacts.findOneById.resolves(undefined);
modelsMock.LivechatContacts.findOneEnabledById.resolves(undefined);
await expect(resolveContactConflicts({ contactId: 'id', customField: { customField: 'newValue' } })).to.be.rejectedWith(
'error-contact-not-found',
);
@ -207,7 +207,7 @@ describe('resolveContactConflicts', () => {
});
it('should throw an error if the contact has no conflicting fields', async () => {
modelsMock.LivechatContacts.findOneById.resolves({
modelsMock.LivechatContacts.findOneEnabledById.resolves({
_id: 'contactId',
name: 'Name',
contactManager: 'contactManagerId',
@ -221,7 +221,7 @@ describe('resolveContactConflicts', () => {
});
it('should throw an error if the contact manager is invalid', async () => {
modelsMock.LivechatContacts.findOneById.resolves({
modelsMock.LivechatContacts.findOneEnabledById.resolves({
_id: 'contactId',
name: 'Name',
contactManager: 'contactManagerId',

@ -15,9 +15,12 @@ export type ResolveContactConflictsParams = {
export async function resolveContactConflicts(params: ResolveContactConflictsParams): Promise<ILivechatContact> {
const { contactId, name, customFields, contactManager, wipeConflicts } = params;
const contact = await LivechatContacts.findOneById<Pick<ILivechatContact, '_id' | 'customFields' | 'conflictingFields'>>(contactId, {
projection: { _id: 1, customFields: 1, conflictingFields: 1 },
});
const contact = await LivechatContacts.findOneEnabledById<Pick<ILivechatContact, '_id' | 'customFields' | 'conflictingFields'>>(
contactId,
{
projection: { _id: 1, customFields: 1, conflictingFields: 1 },
},
);
if (!contact) {
throw new Error('error-contact-not-found');

@ -4,7 +4,7 @@ import sinon from 'sinon';
const modelsMock = {
LivechatContacts: {
findOneById: sinon.stub(),
findOneEnabledById: sinon.stub(),
updateContact: sinon.stub(),
},
LivechatRooms: {
@ -28,19 +28,19 @@ const { updateContact } = proxyquire.noCallThru().load('./updateContact', {
describe('updateContact', () => {
beforeEach(() => {
modelsMock.LivechatContacts.findOneById.reset();
modelsMock.LivechatContacts.findOneEnabledById.reset();
modelsMock.LivechatContacts.updateContact.reset();
modelsMock.LivechatRooms.updateContactDataByContactId.reset();
});
it('should throw an error if the contact does not exist', async () => {
modelsMock.LivechatContacts.findOneById.resolves(undefined);
modelsMock.LivechatContacts.findOneEnabledById.resolves(undefined);
await expect(updateContact('any_id')).to.be.rejectedWith('error-contact-not-found');
expect(modelsMock.LivechatContacts.updateContact.getCall(0)).to.be.null;
});
it('should update the contact with correct params', async () => {
modelsMock.LivechatContacts.findOneById.resolves({ _id: 'contactId' });
modelsMock.LivechatContacts.findOneEnabledById.resolves({ _id: 'contactId' });
modelsMock.LivechatContacts.updateContact.resolves({ _id: 'contactId', name: 'John Doe' } as any);
const updatedContact = await updateContact({ contactId: 'contactId', name: 'John Doe' });

@ -25,7 +25,7 @@ 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' | 'name' | 'customFields' | 'conflictingFields'>>(
const contact = await LivechatContacts.findOneEnabledById<Pick<ILivechatContact, '_id' | 'name' | 'customFields' | 'conflictingFields'>>(
contactId,
{
projection: { _id: 1, name: 1, customFields: 1, conflictingFields: 1 },

@ -78,11 +78,30 @@ export async function saveGuest(
return ret;
}
export async function removeGuest({ _id, token }: { _id: string; token: string }) {
await cleanGuestHistory(token);
async function removeGuest({ _id }: { _id: string }) {
await cleanGuestHistory(_id);
return LivechatVisitors.disableById(_id);
}
export async function removeContactsByVisitorId({ _id }: { _id: string }) {
// A visitor shouldn't have many contacts associated, so we can remove them like this
const contacts = await LivechatContacts.findAllByVisitorId(_id).toArray();
if (!contacts.length) {
livechatLogger.debug({ msg: 'No contacts found for visitor', visitorId: _id });
await removeGuest({ _id });
}
// And a contact shouldn't have many channels associated, so we can do this
livechatLogger.debug({ msg: 'Removing channels for contacts', visitorId: _id, contacts: contacts.map(({ _id }) => _id) });
for await (const contact of contacts) {
for await (const { visitor } of contact.channels) {
await removeGuest({ _id: visitor.visitorId });
}
await LivechatContacts.disableByVisitorId(_id);
}
}
export async function registerGuest(newData: RegisterGuestType): Promise<ILivechatVisitor | null> {
const visitor = await Visitors.registerGuest(newData);
if (!visitor) {
@ -123,14 +142,14 @@ export async function registerGuest(newData: RegisterGuestType): Promise<ILivech
return visitor;
}
async function cleanGuestHistory(token: string) {
async function cleanGuestHistory(_id: string) {
// This shouldn't be possible, but just in case
if (!token) {
if (!_id) {
throw new Error('error-invalid-guest');
}
// TODO: optimize function => instead of removing one by one, fetch the _ids of the rooms and then remove them in bulk
const cursor = LivechatRooms.findByVisitorToken(token, { projection: { _id: 1 } });
const cursor = LivechatRooms.findByVisitorId(_id, { projection: { _id: 1 } });
for await (const room of cursor) {
await Promise.all([
Subscriptions.removeByRoomId(room._id, {
@ -144,9 +163,9 @@ async function cleanGuestHistory(token: string) {
]);
}
await LivechatRooms.removeByVisitorToken(token);
await LivechatRooms.removeByVisitorId(_id);
const livechatInquiries = await LivechatInquiry.findIdsByVisitorToken(token).toArray();
const livechatInquiries = await LivechatInquiry.findIdsByVisitorId(_id).toArray();
await LivechatInquiry.removeByIds(livechatInquiries.map(({ _id }) => _id));
void notifyOnLivechatInquiryChanged(livechatInquiries, 'removed');
}

@ -95,7 +95,7 @@ checkDefaultAgentOnNewRoom.patch(async (_next, defaultAgent, { visitorId, source
}
const contactId = await migrateVisitorIfMissingContact(visitorId, source);
const contact = contactId ? await LivechatContacts.findOneById(contactId, { projection: { contactManager: 1 } }) : undefined;
const contact = contactId ? await LivechatContacts.findOneEnabledById(contactId, { projection: { contactManager: 1 } }) : undefined;
const contactManagerPreferred = settings.get<boolean>('Omnichannel_contact_manager_routing');
const guestManager = contactManagerPreferred && (await getDefaultAgent({ id: contact?.contactManager }));

@ -15,7 +15,7 @@ export const runIsAgentAvailableToTakeContactInquiry = async (
source: IOmnichannelSource,
contactId: ILivechatContact['_id'],
): Promise<{ error: string; value: false } | { value: true }> => {
const contact = await LivechatContacts.findOneById<Pick<ILivechatContact, '_id' | 'unknown' | 'channels'>>(contactId, {
const contact = await LivechatContacts.findOneEnabledById<Pick<ILivechatContact, '_id' | 'unknown' | 'channels'>>(contactId, {
projection: {
unknown: 1,
channels: 1,

@ -15,7 +15,7 @@ export const runMergeContacts = async (
visitor: ILivechatContactVisitorAssociation,
session?: ClientSession,
): Promise<ILivechatContact | null> => {
const originalContact = await LivechatContacts.findOneById(contactId, { session });
const originalContact = await LivechatContacts.findOneEnabledById(contactId, { session });
if (!originalContact) {
throw new Error('error-invalid-contact');
}
@ -57,7 +57,7 @@ export const runMergeContacts = async (
logger.debug({ msg: 'Updating rooms with new contact id', contactId });
await LivechatRooms.updateMergedContactIds(similarContactIds, contactId, { session });
return LivechatContacts.findOneById(contactId, { session });
return LivechatContacts.findOneEnabledById(contactId, { session });
};
mergeContacts.patch(runMergeContacts, () => License.hasModule('contact-id-verification'));

@ -4,7 +4,7 @@ import sinon from 'sinon';
const modelsMock = {
LivechatContacts: {
findOneById: sinon.stub(),
findOneEnabledById: sinon.stub(),
},
};
@ -23,7 +23,7 @@ const { runIsAgentAvailableToTakeContactInquiry } = proxyquire
describe('isAgentAvailableToTakeContactInquiry', () => {
beforeEach(() => {
modelsMock.LivechatContacts.findOneById.reset();
modelsMock.LivechatContacts.findOneEnabledById.reset();
settingsMock.get.reset();
});
@ -32,16 +32,18 @@ describe('isAgentAvailableToTakeContactInquiry', () => {
});
it('should return false if the contact is not found', async () => {
modelsMock.LivechatContacts.findOneById.resolves(undefined);
modelsMock.LivechatContacts.findOneEnabledById.resolves(undefined);
const { value, error } = await runIsAgentAvailableToTakeContactInquiry(() => undefined, 'visitorId', {}, 'rid');
expect(value).to.be.false;
expect(error).to.eq('error-invalid-contact');
expect(modelsMock.LivechatContacts.findOneById.calledOnceWith('contactId', sinon.match({ projection: { unknown: 1, channels: 1 } })));
expect(
modelsMock.LivechatContacts.findOneEnabledById.calledOnceWith('contactId', sinon.match({ projection: { unknown: 1, channels: 1 } })),
);
});
it('should return false if the contact is unknown and Livechat_Block_Unknown_Contacts is true', async () => {
modelsMock.LivechatContacts.findOneById.resolves({ unknown: true });
modelsMock.LivechatContacts.findOneEnabledById.resolves({ unknown: true });
settingsMock.get.withArgs('Livechat_Block_Unknown_Contacts').returns(true);
const { value, error } = await runIsAgentAvailableToTakeContactInquiry(() => undefined, 'visitorId', {}, 'rid');
expect(value).to.be.false;
@ -49,7 +51,7 @@ describe('isAgentAvailableToTakeContactInquiry', () => {
});
it('should return false if the contact is not verified and Livechat_Block_Unverified_Contacts is true', async () => {
modelsMock.LivechatContacts.findOneById.resolves({
modelsMock.LivechatContacts.findOneEnabledById.resolves({
unknown: false,
channels: [
{ verified: false, visitor: { source: { type: 'channelName' }, visitorId: 'visitorId' } },
@ -64,7 +66,7 @@ describe('isAgentAvailableToTakeContactInquiry', () => {
});
it('should return true if the contact has the verified channel', async () => {
modelsMock.LivechatContacts.findOneById.resolves({
modelsMock.LivechatContacts.findOneEnabledById.resolves({
unknown: false,
channels: [
{ verified: true, visitor: { source: { type: 'channelName' }, visitorId: 'visitorId' } },
@ -78,7 +80,7 @@ describe('isAgentAvailableToTakeContactInquiry', () => {
});
it('should not look at the unknown field if the setting Livechat_Block_Unknown_Contacts is false', async () => {
modelsMock.LivechatContacts.findOneById.resolves({
modelsMock.LivechatContacts.findOneEnabledById.resolves({
unknown: true,
channels: [
{ verified: true, visitor: { source: { type: 'channelName' }, visitorId: 'visitorId' } },
@ -92,7 +94,7 @@ describe('isAgentAvailableToTakeContactInquiry', () => {
});
it('should not look at the verified channels if Livechat_Block_Unverified_Contacts is false', async () => {
modelsMock.LivechatContacts.findOneById.resolves({
modelsMock.LivechatContacts.findOneEnabledById.resolves({
unknown: false,
channels: [
{ verified: false, visitor: { source: { type: 'channelName' }, visitorId: 'visitorId' } },
@ -106,7 +108,7 @@ describe('isAgentAvailableToTakeContactInquiry', () => {
});
it('should return true if there is a contact and the settings are false', async () => {
modelsMock.LivechatContacts.findOneById.resolves({
modelsMock.LivechatContacts.findOneEnabledById.resolves({
unknown: false,
channels: [],
});

@ -4,7 +4,7 @@ import sinon from 'sinon';
const modelsMock = {
LivechatContacts: {
findOneById: sinon.stub(),
findOneEnabledById: sinon.stub(),
findSimilarVerifiedContacts: sinon.stub(),
deleteMany: sinon.stub(),
},
@ -45,7 +45,7 @@ describe('mergeContacts', () => {
};
beforeEach(() => {
modelsMock.LivechatContacts.findOneById.reset();
modelsMock.LivechatContacts.findOneEnabledById.reset();
modelsMock.LivechatContacts.findSimilarVerifiedContacts.reset();
modelsMock.LivechatContacts.deleteMany.reset();
modelsMock.LivechatRooms.updateMergedContactIds.reset();
@ -60,7 +60,7 @@ describe('mergeContacts', () => {
});
it('should throw an error if contact does not exist', async () => {
modelsMock.LivechatContacts.findOneById.resolves(undefined);
modelsMock.LivechatContacts.findOneEnabledById.resolves(undefined);
await expect(runMergeContacts(() => undefined, 'invalidId', { visitorId: 'visitorId', source: { type: 'sms' } })).to.be.rejectedWith(
'error-invalid-contact',
@ -68,7 +68,7 @@ describe('mergeContacts', () => {
});
it('should throw an error if contact channel does not exist', async () => {
modelsMock.LivechatContacts.findOneById.resolves({
modelsMock.LivechatContacts.findOneEnabledById.resolves({
_id: 'contactId',
channels: [{ name: 'channelName', visitor: { visitorId: 'visitorId', source: { type: 'sms' } } }],
});
@ -79,12 +79,12 @@ describe('mergeContacts', () => {
});
it('should do nothing if there are no similar verified contacts', async () => {
modelsMock.LivechatContacts.findOneById.resolves({ _id: 'contactId', channels: [targetChannel] });
modelsMock.LivechatContacts.findOneEnabledById.resolves({ _id: 'contactId', channels: [targetChannel] });
modelsMock.LivechatContacts.findSimilarVerifiedContacts.resolves([]);
await runMergeContacts(() => undefined, 'contactId', { visitorId: 'visitorId', source: { type: 'sms' } });
expect(modelsMock.LivechatContacts.findOneById.calledOnceWith('contactId')).to.be.true;
expect(modelsMock.LivechatContacts.findOneEnabledById.calledOnceWith('contactId')).to.be.true;
expect(modelsMock.LivechatContacts.findSimilarVerifiedContacts.calledOnceWith(targetChannel, 'contactId')).to.be.true;
expect(modelsMock.LivechatContacts.deleteMany.notCalled).to.be.true;
expect(contactMergerStub.getAllFieldsFromContact.notCalled).to.be.true;
@ -105,14 +105,14 @@ describe('mergeContacts', () => {
channels: [targetChannel],
};
modelsMock.LivechatContacts.findOneById.resolves(originalContact);
modelsMock.LivechatContacts.findOneEnabledById.resolves(originalContact);
modelsMock.LivechatContacts.findSimilarVerifiedContacts.resolves([similarContact]);
modelsMock.Settings.incrementValueById.resolves({ value: undefined });
await runMergeContacts(() => undefined, 'contactId', { visitorId: 'visitorId', source: { type: 'sms' } });
expect(modelsMock.LivechatContacts.findOneById.calledTwice).to.be.true;
expect(modelsMock.LivechatContacts.findOneById.calledWith('contactId')).to.be.true;
expect(modelsMock.LivechatContacts.findOneEnabledById.calledTwice).to.be.true;
expect(modelsMock.LivechatContacts.findOneEnabledById.calledWith('contactId')).to.be.true;
expect(modelsMock.LivechatContacts.findSimilarVerifiedContacts.calledOnceWith(targetChannel, 'contactId')).to.be.true;
expect(contactMergerStub.getAllFieldsFromContact.calledOnceWith(similarContact)).to.be.true;

@ -747,6 +747,32 @@ describe('LIVECHAT - visitors', () => {
.expect(200);
});
it('should remove the rooms associated with the visitor if any', async () => {
const createdVisitor = await createVisitor();
const room = await createLivechatRoom(createdVisitor.token);
await request
.delete(api(`livechat/visitor/${createdVisitor.token}`))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
await request.get(api('livechat/room')).query({ rid: room._id }).set(credentials).expect(400);
});
it('should remove the contact associated with the visitor if any', async () => {
const createdVisitor = await createVisitor();
const room = await createLivechatRoom(createdVisitor.token);
await request
.delete(api(`livechat/visitor/${createdVisitor.token}`))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
await request.get(api(`omnichannel/contacts.get`)).set(credentials).query({ contactId: room.contactId }).expect(404);
});
it('should return a visitor when the query params is all valid', async () => {
const createdVisitor = await createVisitor();
await request

@ -70,4 +70,8 @@ export interface ILivechatContactsModel extends IBaseModel<ILivechatContact> {
countContactsWithoutChannels(): Promise<number>;
getStatistics(): AggregationCursor<{ totalConflicts: number; avgChannelsPerContact: number }>;
updateByVisitorId(visitorId: string, update: UpdateFilter<ILivechatContact>, options?: UpdateOptions): Promise<UpdateResult>;
disableByVisitorId(visitorId: string): Promise<UpdateResult | Document>;
findOneEnabledById(_id: ILivechatContact['_id'], options?: FindOptions<ILivechatContact>): Promise<ILivechatContact | null>;
findOneEnabledById<P extends Document = ILivechatContact>(_id: P['_id'], options?: FindOptions<P>): Promise<P | null>;
findOneEnabledById(_id: ILivechatContact['_id'], options?: any): Promise<ILivechatContact | null>;
}

@ -23,6 +23,7 @@ export interface ILivechatInquiryModel extends IBaseModel<ILivechatInquiryRecord
): Promise<ILivechatInquiryRecord | null>;
unlock(inquiryId: string): Promise<UpdateResult>;
unlockAll(): Promise<UpdateResult | Document>;
findIdsByVisitorId(_id: ILivechatInquiryRecord['v']['_id']): FindCursor<ILivechatInquiryRecord>;
getCurrentSortedQueueAsync(props: {
inquiryId?: string;
department?: string;

@ -265,6 +265,7 @@ export interface ILivechatRoomsModel extends IBaseModel<IOmnichannelRoom> {
updateVisitorStatus(token: string, status: ILivechatVisitor['status']): Promise<UpdateResult | Document>;
removeAgentByRoomId(roomId: string): Promise<UpdateResult>;
removeByVisitorToken(token: string): Promise<DeleteResult>;
removeByVisitorId(_id: string): Promise<DeleteResult>;
removeById(_id: string): Promise<DeleteResult>;
getVisitorLastMessageTsUpdateQueryByRoomId(lastMessageTs: Date, updater?: Updater<IOmnichannelRoom>): Updater<IOmnichannelRoom>;
setVisitorInactivityInSecondsById(roomId: string, visitorInactivity: any): Promise<UpdateResult>;

@ -116,7 +116,7 @@ export class LivechatContactsRaw extends BaseRaw<ILivechatContact> implements IL
async updateContact(contactId: string, data: Partial<ILivechatContact>, options?: FindOneAndUpdateOptions): Promise<ILivechatContact> {
const updatedValue = await this.findOneAndUpdate(
{ _id: contactId },
{ _id: contactId, enabled: { $ne: false } },
{ $set: { ...data, unknown: false, ...(data.channels && { preRegistration: !data.channels.length }) } },
{ returnDocument: 'after', ...options },
);
@ -124,7 +124,7 @@ export class LivechatContactsRaw extends BaseRaw<ILivechatContact> implements IL
}
updateById(contactId: string, update: UpdateFilter<ILivechatContact>, options?: UpdateOptions): Promise<Document | UpdateResult> {
return this.updateOne({ _id: contactId }, update, options);
return this.updateOne({ _id: contactId, enabled: { $ne: false } }, update, options);
}
async updateContactCustomFields(
@ -137,7 +137,7 @@ export class LivechatContactsRaw extends BaseRaw<ILivechatContact> implements IL
}
return this.findOneAndUpdate(
{ _id: contactId },
{ _id: contactId, enabled: { $ne: false } },
{
$set: { ...dataToUpdate },
},
@ -158,6 +158,7 @@ export class LivechatContactsRaw extends BaseRaw<ILivechatContact> implements IL
{ 'phones.phoneNumber': { $regex: searchRegex, $options: 'i' } },
],
unknown,
enabled: { $ne: false },
};
return this.findPaginated(
@ -225,7 +226,7 @@ export class LivechatContactsRaw extends BaseRaw<ILivechatContact> implements IL
}
async addChannel(contactId: string, channel: ILivechatContactChannel): Promise<void> {
await this.updateOne({ _id: contactId }, { $push: { channels: channel }, $set: { preRegistration: false } });
await this.updateOne({ _id: contactId, enabled: { $ne: false } }, { $push: { channels: channel }, $set: { preRegistration: false } });
}
async updateLastChatById(
@ -305,6 +306,31 @@ export class LivechatContactsRaw extends BaseRaw<ILivechatContact> implements IL
});
}
async findOneEnabledById(_id: ILivechatContact['_id'], options?: FindOptions<ILivechatContact>): Promise<ILivechatContact | null>;
async findOneEnabledById<P extends Document = ILivechatContact>(_id: P['_id'], options?: FindOptions<P>): Promise<P | null>;
async findOneEnabledById(_id: ILivechatContact['_id'], options?: any): Promise<ILivechatContact | null> {
return this.findOne({ _id, enabled: { $ne: false } }, options);
}
disableByVisitorId(visitorId: string): Promise<UpdateResult | Document> {
return this.updateOne(
{ 'channels.visitor.visitorId': visitorId },
{
$set: { enabled: false },
$unset: {
emails: 1,
customFields: 1,
lastChat: 1,
channels: 1,
name: 1,
phones: 1,
},
},
);
}
async addEmail(contactId: string, email: string): Promise<ILivechatContact | null> {
const updatedContact = await this.findOneAndUpdate({ _id: contactId }, { $addToSet: { emails: { address: email } } });

@ -133,6 +133,10 @@ export class LivechatInquiryRaw extends BaseRaw<ILivechatInquiryRecord> implemen
return this.find({ 'v.token': token }, { projection: { _id: 1 } });
}
findIdsByVisitorId(_id: ILivechatInquiryRecord['v']['_id']): FindCursor<ILivechatInquiryRecord> {
return this.find({ 'v._id': _id }, { projection: { _id: 1 } });
}
getDistinctQueuedDepartments(options: AggregateOptions): Promise<{ _id: string | null }[]> {
return this.col
.aggregate<{ _id: string | null }>(

@ -2409,6 +2409,15 @@ export class LivechatRoomsRaw extends BaseRaw<IOmnichannelRoom> implements ILive
return this.deleteMany(query);
}
removeByVisitorId(_id: string) {
const query: Filter<IOmnichannelRoom> = {
't': 'l',
'v._id': _id,
};
return this.deleteMany(query);
}
removeById(_id: string) {
const query: Filter<IOmnichannelRoom> = {
_id,

Loading…
Cancel
Save