diff --git a/.changeset/heavy-snails-help.md b/.changeset/heavy-snails-help.md new file mode 100644 index 000000000000..fb10bac9ea8f --- /dev/null +++ b/.changeset/heavy-snails-help.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/rest-typings": minor +--- + +Implemented "omnichannel/contacts.update" endpoint to update contacts diff --git a/apps/meteor/app/authorization/server/constant/permissions.ts b/apps/meteor/app/authorization/server/constant/permissions.ts index d9ae4133e49e..e5e8f7fb05dd 100644 --- a/apps/meteor/app/authorization/server/constant/permissions.ts +++ b/apps/meteor/app/authorization/server/constant/permissions.ts @@ -97,6 +97,10 @@ export const permissions = [ _id: 'create-livechat-contact', roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'], }, + { + _id: 'update-livechat-contact', + roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'], + }, { _id: 'view-livechat-manager', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, { _id: 'view-omnichannel-contact-center', diff --git a/apps/meteor/app/livechat/server/api/v1/contact.ts b/apps/meteor/app/livechat/server/api/v1/contact.ts index 91b18a6b21af..94bd5ed3e11c 100644 --- a/apps/meteor/app/livechat/server/api/v1/contact.ts +++ b/apps/meteor/app/livechat/server/api/v1/contact.ts @@ -1,11 +1,11 @@ import { LivechatCustomField, LivechatVisitors } from '@rocket.chat/models'; -import { isPOSTOmnichannelContactsProps } from '@rocket.chat/rest-typings'; +import { isPOSTOmnichannelContactsProps, isPOSTUpdateOmnichannelContactsProps } from '@rocket.chat/rest-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { API } from '../../../../api/server'; -import { Contacts, createContact } from '../../lib/Contacts'; +import { Contacts, createContact, updateContact } from '../../lib/Contacts'; API.v1.addRoute( 'omnichannel/contact', @@ -101,3 +101,18 @@ API.v1.addRoute( }, }, ); +API.v1.addRoute( + 'omnichannel/contacts.update', + { authRequired: true, permissionsRequired: ['update-livechat-contact'], validateParams: isPOSTUpdateOmnichannelContactsProps }, + { + async post() { + if (!process.env.TEST_MODE) { + throw new Meteor.Error('error-not-allowed', 'This endpoint is only allowed in test mode'); + } + + const contact = await updateContact({ ...this.bodyParams }); + + return API.v1.success({ contact }); + }, + }, +); diff --git a/apps/meteor/app/livechat/server/lib/Contacts.ts b/apps/meteor/app/livechat/server/lib/Contacts.ts index 4f4a33ee61b2..58404ce27584 100644 --- a/apps/meteor/app/livechat/server/lib/Contacts.ts +++ b/apps/meteor/app/livechat/server/lib/Contacts.ts @@ -1,4 +1,11 @@ -import type { ILivechatContactChannel, ILivechatCustomField, ILivechatVisitor, IOmnichannelRoom, IUser } from '@rocket.chat/core-typings'; +import type { + ILivechatContact, + ILivechatContactChannel, + ILivechatCustomField, + ILivechatVisitor, + IOmnichannelRoom, + IUser, +} from '@rocket.chat/core-typings'; import { LivechatVisitors, Users, @@ -45,6 +52,16 @@ type CreateContactParams = { channels?: ILivechatContactChannel[]; }; +type UpdateContactParams = { + contactId: string; + name?: string; + emails?: string[]; + phones?: string[]; + customFields?: Record; + contactManager?: string; + channels?: ILivechatContactChannel[]; +}; + export const Contacts = { async registerContact({ token, @@ -189,10 +206,7 @@ export async function createContact(params: CreateContactParams): Promise>(contactManager, { projection: { roles: 1 } }); - if (!contactManagerUser) { - throw new Error('error-contact-manager-not-found'); - } + await validateContactManager(contactManager); } const allowedCustomFields = await getAllowedCustomFields(); @@ -211,6 +225,29 @@ export async function createContact(params: CreateContactParams): Promise { + const { contactId, name, emails, phones, customFields, contactManager, channels } = params; + + const contact = await LivechatContacts.findOneById>(contactId, { projection: { _id: 1 } }); + + if (!contact) { + throw new Error('error-contact-not-found'); + } + + if (contactManager) { + await validateContactManager(contactManager); + } + + if (customFields) { + const allowedCustomFields = await getAllowedCustomFields(); + validateCustomFields(allowedCustomFields, customFields); + } + + const updatedContact = await LivechatContacts.updateContact(contactId, { name, emails, phones, contactManager, channels, customFields }); + + return updatedContact; +} + async function getAllowedCustomFields(): Promise { return LivechatCustomField.findByScope( 'visitor', @@ -245,4 +282,18 @@ export function validateCustomFields(allowedCustomFields: ILivechatCustomField[] } } } + + const allowedCustomFieldIds = new Set(allowedCustomFields.map((cf) => cf._id)); + for (const key in customFields) { + if (!allowedCustomFieldIds.has(key)) { + throw new Error(i18n.t('error-custom-field-not-allowed', { key })); + } + } +} + +export async function validateContactManager(contactManagerUserId: string) { + const contactManagerUser = await Users.findOneAgentById>(contactManagerUserId, { projection: { _id: 1 } }); + if (!contactManagerUser) { + throw new Error('error-contact-manager-not-found'); + } } diff --git a/apps/meteor/server/models/raw/LivechatContacts.ts b/apps/meteor/server/models/raw/LivechatContacts.ts index 1f5f29a3cc78..88dac1b9f5c1 100644 --- a/apps/meteor/server/models/raw/LivechatContacts.ts +++ b/apps/meteor/server/models/raw/LivechatContacts.ts @@ -8,4 +8,13 @@ export class LivechatContactsRaw extends BaseRaw implements IL constructor(db: Db, trash?: Collection>) { super(db, 'livechat_contact', trash); } + + async updateContact(contactId: string, data: Partial): Promise { + const updatedValue = await this.findOneAndUpdate( + { _id: contactId }, + { $set: { ...data, unknown: false } }, + { returnDocument: 'after' }, + ); + return updatedValue.value as ILivechatContact; + } } diff --git a/apps/meteor/tests/end-to-end/api/livechat/contacts.ts b/apps/meteor/tests/end-to-end/api/livechat/contacts.ts index 21eced5ee7e9..957d22ba92ae 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/contacts.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/contacts.ts @@ -1,27 +1,42 @@ import { faker } from '@faker-js/faker'; +import type { ILivechatAgent, IUser } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { before, after, describe, it } from 'mocha'; import { getCredentials, api, request, credentials } from '../../../data/api-data'; import { createCustomField, deleteCustomField } from '../../../data/livechat/custom-fields'; import { createAgent } from '../../../data/livechat/rooms'; +import { removeAgent } from '../../../data/livechat/users'; import { removePermissionFromAllRoles, restorePermissionToRoles, updatePermission, updateSetting } from '../../../data/permissions.helper'; import { createUser, deleteUser } from '../../../data/users.helper'; describe('LIVECHAT - contacts', () => { + let agentUser: IUser; + let livechatAgent: ILivechatAgent; before((done) => getCredentials(done)); before(async () => { await updateSetting('Livechat_enabled', true); - await updatePermission('create-livechat-contact', ['admin']); + agentUser = await createUser(); + livechatAgent = await createAgent(agentUser.username); }); after(async () => { + await removeAgent(livechatAgent._id); + await deleteUser(agentUser); await restorePermissionToRoles('create-livechat-contact'); await updateSetting('Livechat_enabled', true); }); describe('[POST] omnichannel/contacts', () => { + before(async () => { + await updatePermission('create-livechat-contact', ['admin']); + }); + + after(async () => { + await restorePermissionToRoles('create-livechat-contact'); + }); + it('should be able to create a new contact', async () => { const res = await request .post(api('omnichannel/contacts')) @@ -92,9 +107,6 @@ describe('LIVECHAT - contacts', () => { }); it('should be able to create a new contact with a contact manager', async () => { - const user = await createUser(); - const livechatAgent = await createAgent(user.username); - const res = await request .post(api('omnichannel/contacts')) .set(credentials) @@ -108,8 +120,6 @@ describe('LIVECHAT - contacts', () => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('contactId'); expect(res.body.contactId).to.be.an('string'); - - await deleteUser(user); }); describe('Custom Fields', () => { @@ -296,4 +306,267 @@ describe('LIVECHAT - contacts', () => { }); }); }); + + describe('[POST] omnichannel/contacts.update', () => { + let contactId: string; + + before(async () => { + const { body } = await request + .post(api('omnichannel/contacts')) + .set(credentials) + .send({ + name: faker.person.fullName(), + emails: [faker.internet.email().toLowerCase()], + phones: [faker.phone.number()], + }); + contactId = body.contactId; + }); + + after(async () => { + await restorePermissionToRoles('update-livechat-contact'); + }); + + it('should be able to update a contact', async () => { + const name = faker.person.fullName(); + const emails = [faker.internet.email().toLowerCase()]; + const phones = [faker.phone.number()]; + + const res = await request.post(api('omnichannel/contacts.update')).set(credentials).send({ + contactId, + name, + emails, + phones, + }); + + expect(res.status).to.be.equal(200); + expect(res.body).to.have.property('success', true); + expect(res.body.contact._id).to.be.equal(contactId); + expect(res.body.contact.name).to.be.equal(name); + expect(res.body.contact.emails).to.be.deep.equal(emails); + expect(res.body.contact.phones).to.be.deep.equal(phones); + }); + + it('should set the unknown field to false when updating a contact', async () => { + const res = await request.post(api('omnichannel/contacts.update')).set(credentials).send({ + contactId, + name: faker.person.fullName(), + }); + + expect(res.status).to.be.equal(200); + expect(res.body).to.have.property('success', true); + expect(res.body.contact._id).to.be.equal(contactId); + expect(res.body.contact.unknown).to.be.equal(false); + }); + + it('should be able to update the contact manager', async () => { + const res = await request.post(api('omnichannel/contacts.update')).set(credentials).send({ + contactId, + contactManager: livechatAgent._id, + }); + + expect(res.status).to.be.equal(200); + expect(res.body).to.have.property('success', true); + expect(res.body.contact._id).to.be.equal(contactId); + expect(res.body.contact.contactManager).to.be.equal(livechatAgent._id); + }); + + it('should return an error if contact does not exist', async () => { + const res = await request + .post(api('omnichannel/contacts.update')) + .set(credentials) + .send({ + contactId: 'invalid', + name: faker.person.fullName(), + emails: [faker.internet.email().toLowerCase()], + phones: [faker.phone.number()], + }); + + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error'); + expect(res.body.error).to.be.equal('error-contact-not-found'); + }); + + it('should return an error if contact manager not exists', async () => { + const res = await request.post(api('omnichannel/contacts.update')).set(credentials).send({ + contactId, + contactManager: 'invalid', + }); + + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error'); + expect(res.body.error).to.be.equal('error-contact-manager-not-found'); + }); + + describe('Permissions', () => { + before(async () => { + await removePermissionFromAllRoles('update-livechat-contact'); + }); + + after(async () => { + await restorePermissionToRoles('update-livechat-contact'); + }); + + it("should return an error if user doesn't have 'update-livechat-contact' permission", async () => { + const res = await request.post(api('omnichannel/contacts.update')).set(credentials).send({ + contactId, + }); + + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error'); + expect(res.body.error).to.be.equal('User does not have the permissions required for this action [error-unauthorized]'); + }); + }); + + describe('Custom Fields', () => { + before(async () => { + await createCustomField({ + field: 'cf1', + label: 'Custom Field 1', + scope: 'visitor', + visibility: 'public', + type: 'input', + required: true, + regexp: '^[0-9]+$', + searchable: true, + public: true, + }); + }); + + after(async () => { + await deleteCustomField('cf1'); + }); + + it('should validate custom fields correctly', async () => { + const res = await request + .post(api('omnichannel/contacts.update')) + .set(credentials) + .send({ + contactId, + customFields: { + cf1: '123', + }, + }); + + expect(res.status).to.be.equal(200); + expect(res.body).to.have.property('success', true); + expect(res.body.contact._id).to.be.equal(contactId); + }); + + it('should return an error for invalid custom field value', async () => { + const res = await request + .post(api('omnichannel/contacts.update')) + .set(credentials) + .send({ + contactId, + customFields: { + cf1: 'invalid', + }, + }); + + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error'); + expect(res.body.error).to.be.equal('Invalid value for Custom Field 1 field'); + }); + + it('should return an error if additional custom fields are provided', async () => { + const res = await request + .post(api('omnichannel/contacts.update')) + .set(credentials) + .send({ + contactId, + customFields: { + cf1: '123', + cf2: 'invalid', + }, + }); + + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error'); + expect(res.body.error).to.be.equal('Custom field cf2 is not allowed'); + }); + }); + + describe('Fields Validation', () => { + it('should return an error if contactId is missing', async () => { + const res = await request + .post(api('omnichannel/contacts.update')) + .set(credentials) + .send({ + emails: [faker.internet.email().toLowerCase()], + phones: [faker.phone.number()], + }); + + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error'); + expect(res.body.error).to.be.equal("must have required property 'contactId' [invalid-params]"); + expect(res.body.errorType).to.be.equal('invalid-params'); + }); + + it('should return an error if emails is not an array', async () => { + const res = await request.post(api('omnichannel/contacts.update')).set(credentials).send({ + contactId, + emails: 'invalid', + }); + + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error'); + expect(res.body.error).to.be.equal('must be array [invalid-params]'); + expect(res.body.errorType).to.be.equal('invalid-params'); + }); + + it('should return an error if emails is not an array of strings', async () => { + const res = await request + .post(api('omnichannel/contacts.update')) + .set(credentials) + .send({ + contactId, + emails: [{ invalid: true }], + }); + + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error'); + expect(res.body.error).to.be.equal('must be string [invalid-params]'); + expect(res.body.errorType).to.be.equal('invalid-params'); + }); + + it('should return an error if phones is not an array', async () => { + const res = await request.post(api('omnichannel/contacts.update')).set(credentials).send({ + contactId, + phones: 'invalid', + }); + + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error'); + expect(res.body.error).to.be.equal('must be array [invalid-params]'); + expect(res.body.errorType).to.be.equal('invalid-params'); + }); + + it('should return an error if phones is not an array of strings', async () => { + const res = await request + .post(api('omnichannel/contacts.update')) + .set(credentials) + .send({ + contactId, + phones: [{ invalid: true }], + }); + + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error'); + expect(res.body.error).to.be.equal('must be string [invalid-params]'); + expect(res.body.errorType).to.be.equal('invalid-params'); + }); + + it('should return an error if additional fields are provided', async () => { + const res = await request.post(api('omnichannel/contacts.update')).set(credentials).send({ + contactId, + unknown: true, + }); + + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error'); + expect(res.body.error).to.be.equal('must NOT have additional properties [invalid-params]'); + expect(res.body.errorType).to.be.equal('invalid-params'); + }); + }); + }); }); diff --git a/apps/meteor/tests/unit/app/livechat/server/lib/Contacts.spec.ts b/apps/meteor/tests/unit/app/livechat/server/lib/Contacts.spec.ts index 9ff2019ffca5..fef3c59469f8 100644 --- a/apps/meteor/tests/unit/app/livechat/server/lib/Contacts.spec.ts +++ b/apps/meteor/tests/unit/app/livechat/server/lib/Contacts.spec.ts @@ -2,10 +2,22 @@ import { expect } from 'chai'; import proxyquire from 'proxyquire'; import sinon from 'sinon'; -const { validateCustomFields } = proxyquire.noCallThru().load('../../../../../../app/livechat/server/lib/Contacts', { - 'meteor/check': sinon.stub(), - 'meteor/meteor': sinon.stub(), -}); +const modelsMock = { + Users: { + findOneAgentById: sinon.stub(), + }, + LivechatContacts: { + findOneById: sinon.stub(), + updateContact: sinon.stub(), + }, +}; +const { validateCustomFields, validateContactManager, updateContact } = proxyquire + .noCallThru() + .load('../../../../../../app/livechat/server/lib/Contacts', { + 'meteor/check': sinon.stub(), + 'meteor/meteor': sinon.stub(), + '@rocket.chat/models': modelsMock, + }); describe('[OC] Contacts', () => { describe('validateCustomFields', () => { @@ -36,5 +48,55 @@ describe('[OC] Contacts', () => { expect(() => validateCustomFields(allowedCustomFields, customFields)).not.to.throw(); }); + + it('should throw an error if a extra custom field is passed', () => { + const allowedCustomFields = [{ _id: 'field1', label: 'Field 1', required: false }]; + const customFields = { field2: 'value' }; + + expect(() => validateCustomFields(allowedCustomFields, customFields)).to.throw(); + }); + }); + + describe('validateContactManager', () => { + beforeEach(() => { + modelsMock.Users.findOneAgentById.reset(); + }); + + it('should throw an error if the user does not exist', async () => { + modelsMock.Users.findOneAgentById.resolves(undefined); + await expect(validateContactManager('any_id')).to.be.rejectedWith('error-contact-manager-not-found'); + }); + + it('should not throw an error if the user has the "livechat-agent" role', async () => { + const user = { _id: 'userId' }; + modelsMock.Users.findOneAgentById.resolves(user); + + await expect(validateContactManager('userId')).to.not.be.rejected; + expect(modelsMock.Users.findOneAgentById.getCall(0).firstArg).to.be.equal('userId'); + }); + }); + + describe('updateContact', () => { + beforeEach(() => { + modelsMock.LivechatContacts.findOneById.reset(); + modelsMock.LivechatContacts.updateContact.reset(); + }); + + it('should throw an error if the contact does not exist', async () => { + modelsMock.LivechatContacts.findOneById.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.updateContact.resolves({ _id: 'contactId', name: 'John Doe' } as any); + + const updatedContact = await updateContact({ contactId: 'contactId', name: 'John Doe' }); + + expect(modelsMock.LivechatContacts.updateContact.getCall(0).args[0]).to.be.equal('contactId'); + expect(modelsMock.LivechatContacts.updateContact.getCall(0).args[1]).to.be.deep.contain({ name: 'John Doe' }); + expect(updatedContact).to.be.deep.equal({ _id: 'contactId', name: 'John Doe' }); + }); }); }); diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index b28b1fc2bfde..ea7e31422cb1 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -2082,6 +2082,7 @@ "error-invalid-custom-field": "Invalid custom field", "error-invalid-custom-field-name": "Invalid custom field name. Use only letters, numbers, hyphens and underscores.", "error-invalid-custom-field-value": "Invalid value for {{field}} field", + "error-custom-field-not-allowed": "Custom field {{key}} is not allowed", "error-invalid-date": "Invalid date provided.", "error-invalid-dates": "From date cannot be after To date", "error-invalid-description": "Invalid description", @@ -5842,6 +5843,8 @@ "view-joined-room": "View Joined Room", "view-joined-room_description": "Permission to view the currently joined channels", "view-l-room": "View Omnichannel Rooms", + "create-livechat-contact": "Create Omnichannel contacts", + "update-livechat-contact": "Update Omnichannel contacts", "view-l-room_description": "Permission to view Omnichannel rooms", "view-livechat-analytics": "View Omnichannel Analytics", "onboarding.page.awaitingConfirmation.subtitle": "We have sent you an email to {{emailAddress}} with a confirmation link. Please verify that the security code below matches the one in the email.", diff --git a/packages/i18n/src/locales/pt-BR.i18n.json b/packages/i18n/src/locales/pt-BR.i18n.json index f072bc626270..dc62318cb230 100644 --- a/packages/i18n/src/locales/pt-BR.i18n.json +++ b/packages/i18n/src/locales/pt-BR.i18n.json @@ -1736,6 +1736,7 @@ "error-invalid-custom-field": "Campo personalizado inválido", "error-invalid-custom-field-name": "Nome inválido para o campo personalizado. Use apenas letras, números, hífens e travessão.", "error-invalid-custom-field-value": "Valor inválido para o campo {{field}}", + "error-custom-field-not-allowed": "O campo personalizado {{key}} não é permitido", "error-invalid-date": "Data fornecida inválida", "error-invalid-description": "Descrição inválida", "error-invalid-domain": "Domínio inválido", @@ -4678,6 +4679,8 @@ "view-joined-room": "Ver sala incorporada", "view-joined-room_description": "Permissão para ver os canais atualmente associados", "view-l-room": "Ver salas de omnichannel", + "create-livechat-contact": "Criar contatos do omnichannel", + "update-livechat-contact": "Atualizar contatos do omnichannel", "view-l-room_description": "Permissão para ver salas de omnichannel", "view-livechat-analytics": "Ver a análise do omnichannel", "onboarding.page.awaitingConfirmation.subtitle": "Enviamos um e-mail para {{emailAddress}} com um link de confirmação. Verifique se o código de segurança abaixo coincide com o do e-mail.", diff --git a/packages/model-typings/src/models/ILivechatContactsModel.ts b/packages/model-typings/src/models/ILivechatContactsModel.ts index bcf48a837400..f94216830884 100644 --- a/packages/model-typings/src/models/ILivechatContactsModel.ts +++ b/packages/model-typings/src/models/ILivechatContactsModel.ts @@ -2,4 +2,6 @@ import type { ILivechatContact } from '@rocket.chat/core-typings'; import type { IBaseModel } from './IBaseModel'; -export type ILivechatContactsModel = IBaseModel; +export interface ILivechatContactsModel extends IBaseModel { + updateContact(contactId: string, data: Partial): Promise; +} diff --git a/packages/rest-typings/src/v1/omnichannel.ts b/packages/rest-typings/src/v1/omnichannel.ts index c15e94030de3..1ed249f5dd55 100644 --- a/packages/rest-typings/src/v1/omnichannel.ts +++ b/packages/rest-typings/src/v1/omnichannel.ts @@ -27,6 +27,7 @@ import type { ReportWithUnmatchingElements, SMSProviderResponse, ILivechatTriggerActionResponse, + ILivechatContact, } from '@rocket.chat/core-typings'; import { ILivechatAgentStatus } from '@rocket.chat/core-typings'; import Ajv from 'ajv'; @@ -1254,6 +1255,55 @@ const POSTOmnichannelContactsSchema = { export const isPOSTOmnichannelContactsProps = ajv.compile(POSTOmnichannelContactsSchema); +type POSTUpdateOmnichannelContactsProps = { + contactId: string; + name?: string; + emails?: string[]; + phones?: string[]; + customFields?: Record; + contactManager?: string; +}; + +const POSTUpdateOmnichannelContactsSchema = { + type: 'object', + properties: { + contactId: { + type: 'string', + }, + name: { + type: 'string', + }, + emails: { + type: 'array', + items: { + type: 'string', + }, + uniqueItems: true, + nullable: true, + }, + phones: { + type: 'array', + items: { + type: 'string', + }, + uniqueItems: true, + nullable: true, + }, + customFields: { + type: 'object', + nullable: true, + }, + contactManager: { + type: 'string', + nullable: true, + }, + }, + required: ['contactId'], + additionalProperties: false, +}; + +export const isPOSTUpdateOmnichannelContactsProps = ajv.compile(POSTUpdateOmnichannelContactsSchema); + type GETOmnichannelContactProps = { contactId: string }; const GETOmnichannelContactSchema = { @@ -3695,6 +3745,9 @@ export type OmnichannelEndpoints = { '/v1/omnichannel/contacts': { POST: (params: POSTOmnichannelContactsProps) => { contactId: string }; }; + '/v1/omnichannel/contacts.update': { + POST: (params: POSTUpdateOmnichannelContactsProps) => { contact: ILivechatContact }; + }; '/v1/omnichannel/contact.search': { GET: (params: GETOmnichannelContactSearchProps) => { contact: ILivechatVisitor | null };