diff --git a/.changeset/tough-points-know.md b/.changeset/tough-points-know.md new file mode 100644 index 00000000000..64d9f7b8896 --- /dev/null +++ b/.changeset/tough-points-know.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/model-typings": patch +"@rocket.chat/models": patch +--- + +Fixes GDPR contact information removal for Omnichannel. diff --git a/apps/meteor/app/apps/server/converters/contacts.ts b/apps/meteor/app/apps/server/converters/contacts.ts index 8dc49a829d0..fbe78936075 100644 --- a/apps/meteor/app/apps/server/converters/contacts.ts +++ b/apps/meteor/app/apps/server/converters/contacts.ts @@ -6,7 +6,7 @@ import { transformMappedData } from './transformMappedData'; export class AppContactsConverter implements IAppContactsConverter { async convertById(contactId: ILivechatContact['_id']): Promise { - const contact = await LivechatContacts.findOneById(contactId); + const contact = await LivechatContacts.findOneEnabledById(contactId); if (!contact) { return; } diff --git a/apps/meteor/app/apps/server/converters/rooms.js b/apps/meteor/app/apps/server/converters/rooms.js index 2404451366a..4152b12bbe5 100644 --- a/apps/meteor/app/apps/server/converters/rooms.js +++ b/apps/meteor/app/apps/server/converters/rooms.js @@ -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; } diff --git a/apps/meteor/app/livechat/server/api/v1/contact.ts b/apps/meteor/app/livechat/server/api/v1/contact.ts index 09218d13e28..76ef84383a1 100644 --- a/apps/meteor/app/livechat/server/api/v1/contact.ts +++ b/apps/meteor/app/livechat/server/api/v1/contact.ts @@ -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(); diff --git a/apps/meteor/app/livechat/server/api/v1/visitor.ts b/apps/meteor/app/livechat/server/api/v1/visitor.ts index 01c6b5026e6..1bc28cdd828 100644 --- a/apps/meteor/app/livechat/server/api/v1/visitor.ts +++ b/apps/meteor/app/livechat/server/api/v1/visitor.ts @@ -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(), - }, - }); }, }); diff --git a/apps/meteor/app/livechat/server/lib/Helper.ts b/apps/meteor/app/livechat/server/lib/Helper.ts index 9fe6ba3c56d..65ae49fb3b0 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.ts +++ b/apps/meteor/app/livechat/server/lib/Helper.ts @@ -98,7 +98,7 @@ export const prepareLivechatRoom = async ( const contactId = await migrateVisitorIfMissingContact(_id, source); const contact = contactId && - (await LivechatContacts.findOneById>(contactId, { + (await LivechatContacts.findOneEnabledById>(contactId, { projection: { name: 1, channels: 1, activity: 1 }, })); if (!contact) { diff --git a/apps/meteor/app/livechat/server/lib/QueueManager.ts b/apps/meteor/app/livechat/server/lib/QueueManager.ts index f85ffba9d6e..5fd12908f5b 100644 --- a/apps/meteor/app/livechat/server/lib/QueueManager.ts +++ b/apps/meteor/app/livechat/server/lib/QueueManager.ts @@ -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; } diff --git a/apps/meteor/app/livechat/server/lib/contacts/getContactChannelsGrouped.ts b/apps/meteor/app/livechat/server/lib/contacts/getContactChannelsGrouped.ts index 9cf83224708..20e3f59993f 100644 --- a/apps/meteor/app/livechat/server/lib/contacts/getContactChannelsGrouped.ts +++ b/apps/meteor/app/livechat/server/lib/contacts/getContactChannelsGrouped.ts @@ -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 { - const contact = await LivechatContacts.findOneById>(contactId, { projection: { channels: 1 } }); + const contact = await LivechatContacts.findOneEnabledById>(contactId, { projection: { channels: 1 } }); if (!contact?.channels) { return []; diff --git a/apps/meteor/app/livechat/server/lib/contacts/getContactHistory.ts b/apps/meteor/app/livechat/server/lib/contacts/getContactHistory.ts index e569ece4bd8..5eee31d0f0b 100644 --- a/apps/meteor/app/livechat/server/lib/contacts/getContactHistory.ts +++ b/apps/meteor/app/livechat/server/lib/contacts/getContactHistory.ts @@ -32,7 +32,7 @@ export const getContactHistory = makeFunction( async (params: GetContactHistoryParams): Promise> => { const { contactId, count, offset, sort } = params; - const contact = await LivechatContacts.findOneById>(contactId, { projection: { _id: 1 } }); + const contact = await LivechatContacts.findOneEnabledById>(contactId, { projection: { _id: 1 } }); if (!contact) { throw new Error('error-contact-not-found'); diff --git a/apps/meteor/app/livechat/server/lib/contacts/resolveContactConflicts.spec.ts b/apps/meteor/app/livechat/server/lib/contacts/resolveContactConflicts.spec.ts index 245d42801f7..9694c8f7e93 100644 --- a/apps/meteor/app/livechat/server/lib/contacts/resolveContactConflicts.spec.ts +++ b/apps/meteor/app/livechat/server/lib/contacts/resolveContactConflicts.spec.ts @@ -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', diff --git a/apps/meteor/app/livechat/server/lib/contacts/resolveContactConflicts.ts b/apps/meteor/app/livechat/server/lib/contacts/resolveContactConflicts.ts index 332db8e29ca..f6d03757531 100644 --- a/apps/meteor/app/livechat/server/lib/contacts/resolveContactConflicts.ts +++ b/apps/meteor/app/livechat/server/lib/contacts/resolveContactConflicts.ts @@ -15,9 +15,12 @@ export type ResolveContactConflictsParams = { export async function resolveContactConflicts(params: ResolveContactConflictsParams): Promise { const { contactId, name, customFields, contactManager, wipeConflicts } = params; - const contact = await LivechatContacts.findOneById>(contactId, { - projection: { _id: 1, customFields: 1, conflictingFields: 1 }, - }); + const contact = await LivechatContacts.findOneEnabledById>( + contactId, + { + projection: { _id: 1, customFields: 1, conflictingFields: 1 }, + }, + ); if (!contact) { throw new Error('error-contact-not-found'); diff --git a/apps/meteor/app/livechat/server/lib/contacts/updateContact.spec.ts b/apps/meteor/app/livechat/server/lib/contacts/updateContact.spec.ts index b6e4bf929a6..348154e9983 100644 --- a/apps/meteor/app/livechat/server/lib/contacts/updateContact.spec.ts +++ b/apps/meteor/app/livechat/server/lib/contacts/updateContact.spec.ts @@ -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' }); diff --git a/apps/meteor/app/livechat/server/lib/contacts/updateContact.ts b/apps/meteor/app/livechat/server/lib/contacts/updateContact.ts index cabb0359796..a7389420d4a 100644 --- a/apps/meteor/app/livechat/server/lib/contacts/updateContact.ts +++ b/apps/meteor/app/livechat/server/lib/contacts/updateContact.ts @@ -25,7 +25,7 @@ export type UpdateContactParams = { export async function updateContact(params: UpdateContactParams): Promise { const { contactId, name, emails, phones, customFields: receivedCustomFields, contactManager, channels, wipeConflicts } = params; - const contact = await LivechatContacts.findOneById>( + const contact = await LivechatContacts.findOneEnabledById>( contactId, { projection: { _id: 1, name: 1, customFields: 1, conflictingFields: 1 }, diff --git a/apps/meteor/app/livechat/server/lib/guests.ts b/apps/meteor/app/livechat/server/lib/guests.ts index 86ddf28fd2c..f4bee437099 100644 --- a/apps/meteor/app/livechat/server/lib/guests.ts +++ b/apps/meteor/app/livechat/server/lib/guests.ts @@ -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 { const visitor = await Visitors.registerGuest(newData); if (!visitor) { @@ -123,14 +142,14 @@ export async function registerGuest(newData: RegisterGuestType): Promise 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'); } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/handleNextAgentPreferredEvents.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/handleNextAgentPreferredEvents.ts index 89c3d8c452b..7aa1f5acd0f 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/handleNextAgentPreferredEvents.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/handleNextAgentPreferredEvents.ts @@ -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('Omnichannel_contact_manager_routing'); const guestManager = contactManagerPreferred && (await getDefaultAgent({ id: contact?.contactManager })); diff --git a/apps/meteor/ee/server/patches/isAgentAvailableToTakeContactInquiry.ts b/apps/meteor/ee/server/patches/isAgentAvailableToTakeContactInquiry.ts index 4f85d07f5b1..8c44b3a9968 100644 --- a/apps/meteor/ee/server/patches/isAgentAvailableToTakeContactInquiry.ts +++ b/apps/meteor/ee/server/patches/isAgentAvailableToTakeContactInquiry.ts @@ -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>(contactId, { + const contact = await LivechatContacts.findOneEnabledById>(contactId, { projection: { unknown: 1, channels: 1, diff --git a/apps/meteor/ee/server/patches/mergeContacts.ts b/apps/meteor/ee/server/patches/mergeContacts.ts index f692011a8a6..b22b76813e0 100644 --- a/apps/meteor/ee/server/patches/mergeContacts.ts +++ b/apps/meteor/ee/server/patches/mergeContacts.ts @@ -15,7 +15,7 @@ export const runMergeContacts = async ( visitor: ILivechatContactVisitorAssociation, session?: ClientSession, ): Promise => { - 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')); diff --git a/apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/lib/isAgentAvailableToTakeContactInquiry.spec.ts b/apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/lib/isAgentAvailableToTakeContactInquiry.spec.ts index d2b104ea292..0d665895858 100644 --- a/apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/lib/isAgentAvailableToTakeContactInquiry.spec.ts +++ b/apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/lib/isAgentAvailableToTakeContactInquiry.spec.ts @@ -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: [], }); diff --git a/apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/lib/mergeContacts.spec.ts b/apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/lib/mergeContacts.spec.ts index 5e4b285c7a3..dad4ceaf135 100644 --- a/apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/lib/mergeContacts.spec.ts +++ b/apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/lib/mergeContacts.spec.ts @@ -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; diff --git a/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts b/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts index 484b7ff2713..bc4f52c34e1 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts @@ -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 diff --git a/packages/model-typings/src/models/ILivechatContactsModel.ts b/packages/model-typings/src/models/ILivechatContactsModel.ts index 6352ed9db94..237ad953d8a 100644 --- a/packages/model-typings/src/models/ILivechatContactsModel.ts +++ b/packages/model-typings/src/models/ILivechatContactsModel.ts @@ -70,4 +70,8 @@ export interface ILivechatContactsModel extends IBaseModel { countContactsWithoutChannels(): Promise; getStatistics(): AggregationCursor<{ totalConflicts: number; avgChannelsPerContact: number }>; updateByVisitorId(visitorId: string, update: UpdateFilter, options?: UpdateOptions): Promise; + disableByVisitorId(visitorId: string): Promise; + findOneEnabledById(_id: ILivechatContact['_id'], options?: FindOptions): Promise; + findOneEnabledById

(_id: P['_id'], options?: FindOptions

): Promise

; + findOneEnabledById(_id: ILivechatContact['_id'], options?: any): Promise; } diff --git a/packages/model-typings/src/models/ILivechatInquiryModel.ts b/packages/model-typings/src/models/ILivechatInquiryModel.ts index 5b5283aec61..10c1bc76c8c 100644 --- a/packages/model-typings/src/models/ILivechatInquiryModel.ts +++ b/packages/model-typings/src/models/ILivechatInquiryModel.ts @@ -23,6 +23,7 @@ export interface ILivechatInquiryModel extends IBaseModel; unlock(inquiryId: string): Promise; unlockAll(): Promise; + findIdsByVisitorId(_id: ILivechatInquiryRecord['v']['_id']): FindCursor; getCurrentSortedQueueAsync(props: { inquiryId?: string; department?: string; diff --git a/packages/model-typings/src/models/ILivechatRoomsModel.ts b/packages/model-typings/src/models/ILivechatRoomsModel.ts index 2d242e0e1e7..47d38e0e307 100644 --- a/packages/model-typings/src/models/ILivechatRoomsModel.ts +++ b/packages/model-typings/src/models/ILivechatRoomsModel.ts @@ -265,6 +265,7 @@ export interface ILivechatRoomsModel extends IBaseModel { updateVisitorStatus(token: string, status: ILivechatVisitor['status']): Promise; removeAgentByRoomId(roomId: string): Promise; removeByVisitorToken(token: string): Promise; + removeByVisitorId(_id: string): Promise; removeById(_id: string): Promise; getVisitorLastMessageTsUpdateQueryByRoomId(lastMessageTs: Date, updater?: Updater): Updater; setVisitorInactivityInSecondsById(roomId: string, visitorInactivity: any): Promise; diff --git a/packages/models/src/models/LivechatContacts.ts b/packages/models/src/models/LivechatContacts.ts index 20934dbe304..c1e0c47098f 100644 --- a/packages/models/src/models/LivechatContacts.ts +++ b/packages/models/src/models/LivechatContacts.ts @@ -116,7 +116,7 @@ export class LivechatContactsRaw extends BaseRaw implements IL async updateContact(contactId: string, data: Partial, options?: FindOneAndUpdateOptions): Promise { 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 implements IL } updateById(contactId: string, update: UpdateFilter, options?: UpdateOptions): Promise { - 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 implements IL } return this.findOneAndUpdate( - { _id: contactId }, + { _id: contactId, enabled: { $ne: false } }, { $set: { ...dataToUpdate }, }, @@ -158,6 +158,7 @@ export class LivechatContactsRaw extends BaseRaw implements IL { 'phones.phoneNumber': { $regex: searchRegex, $options: 'i' } }, ], unknown, + enabled: { $ne: false }, }; return this.findPaginated( @@ -225,7 +226,7 @@ export class LivechatContactsRaw extends BaseRaw implements IL } async addChannel(contactId: string, channel: ILivechatContactChannel): Promise { - 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 implements IL }); } + async findOneEnabledById(_id: ILivechatContact['_id'], options?: FindOptions): Promise; + + async findOneEnabledById

(_id: P['_id'], options?: FindOptions

): Promise

; + + async findOneEnabledById(_id: ILivechatContact['_id'], options?: any): Promise { + return this.findOne({ _id, enabled: { $ne: false } }, options); + } + + disableByVisitorId(visitorId: string): Promise { + 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 { const updatedContact = await this.findOneAndUpdate({ _id: contactId }, { $addToSet: { emails: { address: email } } }); diff --git a/packages/models/src/models/LivechatInquiry.ts b/packages/models/src/models/LivechatInquiry.ts index 79d581ce888..918f66f8404 100644 --- a/packages/models/src/models/LivechatInquiry.ts +++ b/packages/models/src/models/LivechatInquiry.ts @@ -133,6 +133,10 @@ export class LivechatInquiryRaw extends BaseRaw implemen return this.find({ 'v.token': token }, { projection: { _id: 1 } }); } + findIdsByVisitorId(_id: ILivechatInquiryRecord['v']['_id']): FindCursor { + return this.find({ 'v._id': _id }, { projection: { _id: 1 } }); + } + getDistinctQueuedDepartments(options: AggregateOptions): Promise<{ _id: string | null }[]> { return this.col .aggregate<{ _id: string | null }>( diff --git a/packages/models/src/models/LivechatRooms.ts b/packages/models/src/models/LivechatRooms.ts index 0bc848947b1..e9fdef490b1 100644 --- a/packages/models/src/models/LivechatRooms.ts +++ b/packages/models/src/models/LivechatRooms.ts @@ -2409,6 +2409,15 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive return this.deleteMany(query); } + removeByVisitorId(_id: string) { + const query: Filter = { + 't': 'l', + 'v._id': _id, + }; + + return this.deleteMany(query); + } + removeById(_id: string) { const query: Filter = { _id,