feat(conversations): add opt-in merge for custom attributes (#15119)
Adds an opt-in merge flag to the conversation custom attributes endpoint
so integrations can update only the keys they send instead of replacing
the whole hash, matching how the contacts endpoint already behaves. Also
adds a destroy_custom_attributes endpoint to remove specific keys,
mirroring the contacts convention. Replace stays the default, so
existing integrations are unaffected.
How to test
POST /conversations/:id/custom_attributes com { "custom_attributes":
{"a":1} }, then { "custom_attributes": {"b":2}, "merge": true } results
in {a:1, b:2}; no merge, results in {b:2}.
POST /conversations/:id/destroy_custom_attributes with {
"custom_attributes": ["a"] } removes only a.
---------
Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
This commit is contained in:
@@ -2,6 +2,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
include Events::Types
|
||||
include DateRangeHelper
|
||||
include HmacConcern
|
||||
include ConversationCustomAttributesConcern
|
||||
|
||||
before_action :conversation, except: [:index, :meta, :search, :create, :filter]
|
||||
before_action :inbox, :contact, :contact_inbox, only: [:create]
|
||||
@@ -129,11 +130,6 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
update_last_seen_on_conversation(last_seen_at, true)
|
||||
end
|
||||
|
||||
def custom_attributes
|
||||
@conversation.custom_attributes = params.permit(custom_attributes: {})[:custom_attributes]
|
||||
@conversation.save!
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize @conversation, :destroy?
|
||||
::Conversations::DeleteService.new(conversation: @conversation, user: Current.user, ip: request.ip).perform
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
module ConversationCustomAttributesConcern
|
||||
def custom_attributes
|
||||
attributes = params.permit(custom_attributes: {})[:custom_attributes]
|
||||
# When `merge` is truthy, only the keys sent are updated and the rest are kept, matching the contacts endpoint.
|
||||
# Replace stays the default so existing integrations are unaffected.
|
||||
attributes = @conversation.custom_attributes.merge(attributes || {}) if ActiveModel::Type::Boolean.new.cast(params[:merge])
|
||||
@conversation.custom_attributes = attributes
|
||||
@conversation.save!
|
||||
end
|
||||
|
||||
def destroy_custom_attributes
|
||||
@conversation.custom_attributes = @conversation.custom_attributes.excluding(params[:custom_attributes])
|
||||
@conversation.save!
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1 @@
|
||||
json.custom_attributes @conversation.custom_attributes
|
||||
@@ -173,6 +173,7 @@ Rails.application.routes.draw do
|
||||
post :update_last_seen
|
||||
post :unread
|
||||
post :custom_attributes
|
||||
post :destroy_custom_attributes
|
||||
get :attachments
|
||||
get :inbox_assistant
|
||||
get :reporting_events if ChatwootApp.enterprise?
|
||||
|
||||
@@ -1212,6 +1212,30 @@ RSpec.describe 'Conversations API', type: :request do
|
||||
expect(conversation.reload.custom_attributes).not_to be_nil
|
||||
expect(conversation.reload.custom_attributes.count).to eq 3
|
||||
end
|
||||
|
||||
it 'merges custom attributes when merge is enabled' do
|
||||
conversation.update!(custom_attributes: { existing_key: 'keep', user_id: 1 })
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/custom_attributes",
|
||||
headers: agent.create_new_auth_token,
|
||||
params: { custom_attributes: { user_id: 1001 }, merge: true },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(conversation.reload.custom_attributes).to eq({ 'existing_key' => 'keep', 'user_id' => 1001 })
|
||||
end
|
||||
|
||||
it 'replaces custom attributes by default' do
|
||||
conversation.update!(custom_attributes: { existing_key: 'gone' })
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/custom_attributes",
|
||||
headers: agent.create_new_auth_token,
|
||||
params: { custom_attributes: { user_id: 1001 } },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(conversation.reload.custom_attributes).to eq({ 'user_id' => 1001 })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is a bot' do
|
||||
@@ -1236,6 +1260,37 @@ RSpec.describe 'Conversations API', type: :request do
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/accounts/{account.id}/conversations/:id/destroy_custom_attributes' do
|
||||
let(:conversation) { create(:conversation, account: account, custom_attributes: { test: 'test', test1: 'test1' }) }
|
||||
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/destroy_custom_attributes"
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
|
||||
before do
|
||||
create(:inbox_member, user: agent, inbox: conversation.inbox)
|
||||
end
|
||||
|
||||
it 'deletes the given custom attribute' do
|
||||
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/destroy_custom_attributes",
|
||||
headers: agent.create_new_auth_token,
|
||||
params: { custom_attributes: ['test'] },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(conversation.reload.custom_attributes).to eq({ 'test1' => 'test1' })
|
||||
expect(response.parsed_body['custom_attributes']).to eq({ 'test1' => 'test1' })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v1/accounts/{account.id}/conversations/:id/attachments' do
|
||||
let(:conversation) { create(:conversation, account: account) }
|
||||
|
||||
|
||||
@@ -20,6 +20,13 @@ requestBody:
|
||||
example:
|
||||
order_id: '12345'
|
||||
previous_conversation: '67890'
|
||||
merge:
|
||||
type: boolean
|
||||
default: false
|
||||
description: >-
|
||||
When true, only the keys sent in `custom_attributes` are updated and the remaining
|
||||
keys are kept intact. When false (default), the whole custom attributes hash is
|
||||
replaced. Use the destroy_custom_attributes endpoint to remove keys.
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
tags:
|
||||
- Conversations
|
||||
operationId: destroy-custom-attributes-of-a-conversation
|
||||
summary: Destroy Custom Attributes
|
||||
description: Removes the given custom attribute keys from a conversation
|
||||
security:
|
||||
- userApiKey: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- custom_attributes
|
||||
properties:
|
||||
custom_attributes:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: The list of custom attribute keys to remove from the conversation
|
||||
example:
|
||||
- order_id
|
||||
- previous_conversation
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
custom_attributes:
|
||||
type: object
|
||||
description: The remaining custom attributes of the conversation
|
||||
'401':
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/bad_request_error'
|
||||
'404':
|
||||
description: Conversation not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/bad_request_error'
|
||||
@@ -381,6 +381,13 @@
|
||||
post:
|
||||
$ref: ./application/conversation/custom_attributes.yml
|
||||
|
||||
/api/v1/accounts/{account_id}/conversations/{conversation_id}/destroy_custom_attributes:
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/account_id'
|
||||
- $ref: '#/components/parameters/conversation_id'
|
||||
post:
|
||||
$ref: ./application/conversation/destroy_custom_attributes.yml
|
||||
|
||||
# Conversations Assignments
|
||||
|
||||
/api/v1/accounts/{account_id}/conversations/{conversation_id}/assignments:
|
||||
|
||||
@@ -5063,6 +5063,11 @@
|
||||
"order_id": "12345",
|
||||
"previous_conversation": "67890"
|
||||
}
|
||||
},
|
||||
"merge": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "When true, only the keys sent in `custom_attributes` are updated and the remaining keys are kept intact. When false (default), the whole custom attributes hash is replaced. Use the destroy_custom_attributes endpoint to remove keys."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5109,6 +5114,93 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/accounts/{account_id}/conversations/{conversation_id}/destroy_custom_attributes": {
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/account_id"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/parameters/conversation_id"
|
||||
}
|
||||
],
|
||||
"post": {
|
||||
"tags": [
|
||||
"Conversations"
|
||||
],
|
||||
"operationId": "destroy-custom-attributes-of-a-conversation",
|
||||
"summary": "Destroy Custom Attributes",
|
||||
"description": "Removes the given custom attribute keys from a conversation",
|
||||
"security": [
|
||||
{
|
||||
"userApiKey": []
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"custom_attributes"
|
||||
],
|
||||
"properties": {
|
||||
"custom_attributes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The list of custom attribute keys to remove from the conversation",
|
||||
"example": [
|
||||
"order_id",
|
||||
"previous_conversation"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"custom_attributes": {
|
||||
"type": "object",
|
||||
"description": "The remaining custom attributes of the conversation"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/bad_request_error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Conversation not found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/bad_request_error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/accounts/{account_id}/conversations/{conversation_id}/assignments": {
|
||||
"parameters": [
|
||||
{
|
||||
|
||||
@@ -3606,6 +3606,11 @@
|
||||
"order_id": "12345",
|
||||
"previous_conversation": "67890"
|
||||
}
|
||||
},
|
||||
"merge": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "When true, only the keys sent in `custom_attributes` are updated and the remaining keys are kept intact. When false (default), the whole custom attributes hash is replaced. Use the destroy_custom_attributes endpoint to remove keys."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3652,6 +3657,93 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/accounts/{account_id}/conversations/{conversation_id}/destroy_custom_attributes": {
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/account_id"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/parameters/conversation_id"
|
||||
}
|
||||
],
|
||||
"post": {
|
||||
"tags": [
|
||||
"Conversations"
|
||||
],
|
||||
"operationId": "destroy-custom-attributes-of-a-conversation",
|
||||
"summary": "Destroy Custom Attributes",
|
||||
"description": "Removes the given custom attribute keys from a conversation",
|
||||
"security": [
|
||||
{
|
||||
"userApiKey": []
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"custom_attributes"
|
||||
],
|
||||
"properties": {
|
||||
"custom_attributes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The list of custom attribute keys to remove from the conversation",
|
||||
"example": [
|
||||
"order_id",
|
||||
"previous_conversation"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"custom_attributes": {
|
||||
"type": "object",
|
||||
"description": "The remaining custom attributes of the conversation"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/bad_request_error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Conversation not found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/bad_request_error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/accounts/{account_id}/conversations/{conversation_id}/assignments": {
|
||||
"parameters": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user