diff --git a/.changeset/rich-steaks-promise.md b/.changeset/rich-steaks-promise.md new file mode 100644 index 0000000000000..20f8767a7b2c7 --- /dev/null +++ b/.changeset/rich-steaks-promise.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/model-typings": patch +"@rocket.chat/models": patch +--- + +Fixed contacts being marked as `known` after editing a custom field, or resolving conflicts by adding a new model function that only updates the `customFields` or `conflictingFields` prop. diff --git a/apps/meteor/app/livechat/server/lib/custom-fields.ts b/apps/meteor/app/livechat/server/lib/custom-fields.ts index ad82ea4dd7d74..07f96b5f4910f 100644 --- a/apps/meteor/app/livechat/server/lib/custom-fields.ts +++ b/apps/meteor/app/livechat/server/lib/custom-fields.ts @@ -30,7 +30,7 @@ export async function updateContactsCustomFields(contact: ILivechatContact, key: contact.conflictingFields.push({ field: `customFields.${key}`, value }); } - await LivechatContacts.updateContact(contact._id, { + await LivechatContacts.updateContactCustomFields(contact._id, { ...(shouldUpdateCustomFields && { customFields: contact.customFields }), ...(contact.conflictingFields && { conflictingFields: contact.conflictingFields }), }); diff --git a/apps/meteor/tests/unit/app/livechat/server/lib/custom-fields.spec.ts b/apps/meteor/tests/unit/app/livechat/server/lib/custom-fields.spec.ts index 7b061f0e6068a..89bf138c1b723 100644 --- a/apps/meteor/tests/unit/app/livechat/server/lib/custom-fields.spec.ts +++ b/apps/meteor/tests/unit/app/livechat/server/lib/custom-fields.spec.ts @@ -5,7 +5,7 @@ import sinon from 'sinon'; const modelsMock = { LivechatContacts: { - updateContact: sinon.stub(), + updateContactCustomFields: sinon.stub(), }, }; @@ -15,7 +15,7 @@ const { updateContactsCustomFields } = proxyquire.noCallThru().load('../../../.. describe('[Custom Fields] updateContactsCustomFields', () => { beforeEach(() => { - modelsMock.LivechatContacts.updateContact.reset(); + modelsMock.LivechatContacts.updateContactCustomFields.reset(); }); it('should not add conflictingFields to the update data when its nullish', async () => { @@ -26,13 +26,13 @@ describe('[Custom Fields] updateContactsCustomFields', () => { }, }; - modelsMock.LivechatContacts.updateContact.resolves({ ...contact, customFields: { customField: 'newValue' } }); + modelsMock.LivechatContacts.updateContactCustomFields.resolves({ ...contact, customFields: { customField: 'newValue' } }); await updateContactsCustomFields(contact, 'customField', 'newValue', true); - expect(modelsMock.LivechatContacts.updateContact.calledOnce).to.be.true; - expect(modelsMock.LivechatContacts.updateContact.getCall(0).args[0]).to.be.equal('contactId'); - expect(modelsMock.LivechatContacts.updateContact.getCall(0).args[1]).to.deep.equal({ + expect(modelsMock.LivechatContacts.updateContactCustomFields.calledOnce).to.be.true; + expect(modelsMock.LivechatContacts.updateContactCustomFields.getCall(0).args[0]).to.be.equal('contactId'); + expect(modelsMock.LivechatContacts.updateContactCustomFields.getCall(0).args[1]).to.deep.equal({ customFields: { customField: 'newValue' }, }); }); @@ -45,16 +45,16 @@ describe('[Custom Fields] updateContactsCustomFields', () => { }, }; - modelsMock.LivechatContacts.updateContact.resolves({ + modelsMock.LivechatContacts.updateContactCustomFields.resolves({ ...contact, conflictingFields: [{ field: 'customFields.customField', value: 'newValue' }], }); await updateContactsCustomFields(contact, 'customField', 'newValue', false); - expect(modelsMock.LivechatContacts.updateContact.calledOnce).to.be.true; - expect(modelsMock.LivechatContacts.updateContact.getCall(0).args[0]).to.be.equal('contactId'); - expect(modelsMock.LivechatContacts.updateContact.getCall(0).args[1]).to.deep.equal({ + expect(modelsMock.LivechatContacts.updateContactCustomFields.calledOnce).to.be.true; + expect(modelsMock.LivechatContacts.updateContactCustomFields.getCall(0).args[0]).to.be.equal('contactId'); + expect(modelsMock.LivechatContacts.updateContactCustomFields.getCall(0).args[1]).to.deep.equal({ conflictingFields: [{ field: 'customFields.customField', value: 'newValue' }], }); }); diff --git a/packages/model-typings/src/models/ILivechatContactsModel.ts b/packages/model-typings/src/models/ILivechatContactsModel.ts index b93ec3b304bef..6352ed9db94db 100644 --- a/packages/model-typings/src/models/ILivechatContactsModel.ts +++ b/packages/model-typings/src/models/ILivechatContactsModel.ts @@ -25,6 +25,7 @@ export interface ILivechatContactsModel extends IBaseModel { ): Promise; updateContact(contactId: string, data: Partial, options?: FindOneAndUpdateOptions): Promise; updateById(contactId: string, update: UpdateFilter, options?: UpdateOptions): Promise; + updateContactCustomFields(contactId: string, data: Partial, options?: UpdateOptions): Promise; addChannel(contactId: string, channel: ILivechatContactChannel): Promise; findPaginatedContacts( search: { searchText?: string; unknown?: boolean }, diff --git a/packages/models/src/models/LivechatContacts.ts b/packages/models/src/models/LivechatContacts.ts index 927d7b82a6bef..20934dbe304c5 100644 --- a/packages/models/src/models/LivechatContacts.ts +++ b/packages/models/src/models/LivechatContacts.ts @@ -2,6 +2,7 @@ import type { AtLeast, ILivechatContact, ILivechatContactChannel, + ILivechatContactConflictingField, ILivechatContactVisitorAssociation, ILivechatVisitor, RocketChatRecordDeleted, @@ -126,6 +127,24 @@ export class LivechatContactsRaw extends BaseRaw implements IL return this.updateOne({ _id: contactId }, update, options); } + async updateContactCustomFields( + contactId: string, + dataToUpdate: { customFields: Record; conflictingFields: ILivechatContactConflictingField[] }, + options?: FindOneAndUpdateOptions, + ): Promise { + if (!dataToUpdate.customFields && !dataToUpdate.conflictingFields) { + throw new Error('At least one of customFields or conflictingFields must be provided'); + } + + return this.findOneAndUpdate( + { _id: contactId }, + { + $set: { ...dataToUpdate }, + }, + { returnDocument: 'after', ...options }, + ); + } + findPaginatedContacts( search: { searchText?: string; unknown?: boolean }, options?: FindOptions,