feat: Add support for conversation attribute automation conditions in message event (#4518)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
parent
b082b0e58c
commit
2acb48bbe0
7 changed files with 255 additions and 37 deletions
|
@ -9,6 +9,7 @@ class Messages::MessageBuilder
|
||||||
@user = user
|
@user = user
|
||||||
@message_type = params[:message_type] || 'outgoing'
|
@message_type = params[:message_type] || 'outgoing'
|
||||||
@attachments = params[:attachments]
|
@attachments = params[:attachments]
|
||||||
|
@automation_rule = @params&.dig(:content_attributes, :automation_rule_id)
|
||||||
return unless params.instance_of?(ActionController::Parameters)
|
return unless params.instance_of?(ActionController::Parameters)
|
||||||
|
|
||||||
@in_reply_to = params.to_unsafe_h&.dig(:content_attributes, :in_reply_to)
|
@in_reply_to = params.to_unsafe_h&.dig(:content_attributes, :in_reply_to)
|
||||||
|
@ -64,6 +65,10 @@ class Messages::MessageBuilder
|
||||||
@params[:external_created_at].present? ? { external_created_at: @params[:external_created_at] } : {}
|
@params[:external_created_at].present? ? { external_created_at: @params[:external_created_at] } : {}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def automation_rule_id
|
||||||
|
@automation_rule.present? ? { content_attributes: { automation_rule_id: @automation_rule } } : {}
|
||||||
|
end
|
||||||
|
|
||||||
def message_sender
|
def message_sender
|
||||||
return if @params[:sender_type] != 'AgentBot'
|
return if @params[:sender_type] != 'AgentBot'
|
||||||
|
|
||||||
|
@ -82,6 +87,6 @@ class Messages::MessageBuilder
|
||||||
items: @items,
|
items: @items,
|
||||||
in_reply_to: @in_reply_to,
|
in_reply_to: @in_reply_to,
|
||||||
echo_id: @params[:echo_id]
|
echo_id: @params[:echo_id]
|
||||||
}.merge(external_created_at)
|
}.merge(external_created_at).merge(automation_rule_id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -64,6 +64,13 @@ export const AUTOMATIONS = {
|
||||||
inputType: 'plain_text',
|
inputType: 'plain_text',
|
||||||
filterOperators: OPERATOR_TYPES_2,
|
filterOperators: OPERATOR_TYPES_2,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'inbox_id',
|
||||||
|
name: 'Inbox',
|
||||||
|
attributeI18nKey: 'INBOX',
|
||||||
|
inputType: 'multi_select',
|
||||||
|
filterOperators: OPERATOR_TYPES_1,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
|
@ -149,6 +156,13 @@ export const AUTOMATIONS = {
|
||||||
inputType: 'plain_text',
|
inputType: 'plain_text',
|
||||||
filterOperators: OPERATOR_TYPES_2,
|
filterOperators: OPERATOR_TYPES_2,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'inbox_id',
|
||||||
|
name: 'Inbox',
|
||||||
|
attributeI18nKey: 'INBOX',
|
||||||
|
inputType: 'multi_select',
|
||||||
|
filterOperators: OPERATOR_TYPES_1,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
|
@ -247,6 +261,13 @@ export const AUTOMATIONS = {
|
||||||
inputType: 'search_select',
|
inputType: 'search_select',
|
||||||
filterOperators: OPERATOR_TYPES_3,
|
filterOperators: OPERATOR_TYPES_3,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'inbox_id',
|
||||||
|
name: 'Inbox',
|
||||||
|
attributeI18nKey: 'INBOX',
|
||||||
|
inputType: 'multi_select',
|
||||||
|
filterOperators: OPERATOR_TYPES_1,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
|
|
|
@ -36,7 +36,7 @@ class AutomationRuleListener < BaseListener
|
||||||
return unless rule_present?('message_created', account)
|
return unless rule_present?('message_created', account)
|
||||||
|
|
||||||
@rules.each do |rule|
|
@rules.each do |rule|
|
||||||
conditions_match = ::AutomationRules::ConditionsFilterService.new(rule, message.conversation, { message: message }).message_conditions
|
conditions_match = ::AutomationRules::ConditionsFilterService.new(rule, message.conversation, { message: message }).perform
|
||||||
::AutomationRules::ActionService.new(rule, account, message.conversation).perform if conditions_match.present?
|
::AutomationRules::ActionService.new(rule, account, message.conversation).perform if conditions_match.present?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -27,7 +27,7 @@ class AutomationRule < ApplicationRecord
|
||||||
|
|
||||||
scope :active, -> { where(active: true) }
|
scope :active, -> { where(active: true) }
|
||||||
|
|
||||||
CONDITIONS_ATTRS = %w[content email country_code status message_type browser_language assignee_id team_id referer city company].freeze
|
CONDITIONS_ATTRS = %w[content email country_code status message_type browser_language assignee_id team_id referer city company inbox_id].freeze
|
||||||
ACTIONS_ATTRS = %w[send_message add_label send_email_to_team assign_team assign_best_agents send_attachments].freeze
|
ACTIONS_ATTRS = %w[send_message add_label send_email_to_team assign_team assign_best_agents send_attachments].freeze
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -56,7 +56,7 @@ class AutomationRules::ActionService
|
||||||
end
|
end
|
||||||
|
|
||||||
def send_message(message)
|
def send_message(message)
|
||||||
params = { content: message[0], private: false }
|
params = { content: message[0], private: false, content_attributes: { automation_rule_id: @rule.id } }
|
||||||
mb = Messages::MessageBuilder.new(nil, @conversation, params)
|
mb = Messages::MessageBuilder.new(nil, @conversation, params)
|
||||||
mb.perform
|
mb.perform
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,36 +14,33 @@ class AutomationRules::ConditionsFilterService < FilterService
|
||||||
end
|
end
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
conversation_filters = @filters['conversations']
|
@conversation_filters = @filters['conversations']
|
||||||
contact_filters = @filters['contacts']
|
@contact_filters = @filters['contacts']
|
||||||
|
@message_filters = @filters['messages']
|
||||||
|
|
||||||
@rule.conditions.each_with_index do |query_hash, current_index|
|
@rule.conditions.each_with_index do |query_hash, current_index|
|
||||||
conversation_filter = conversation_filters[query_hash['attribute_key']]
|
apply_filter(query_hash, current_index)
|
||||||
contact_filter = contact_filters[query_hash['attribute_key']]
|
|
||||||
|
|
||||||
if conversation_filter
|
|
||||||
@query_string += conversation_query_string('conversations', conversation_filter, query_hash.with_indifferent_access, current_index)
|
|
||||||
elsif contact_filter
|
|
||||||
@query_string += conversation_query_string('contacts', contact_filter, query_hash.with_indifferent_access, current_index)
|
|
||||||
elsif custom_attribute(query_hash['attribute_key'], @account)
|
|
||||||
# send table name according to attribute key right now we are supporting contact based custom attribute filter
|
|
||||||
@query_string += custom_attribute_query(query_hash.with_indifferent_access, 'contacts', current_index)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
records = base_relation.where(@query_string, @filter_values.with_indifferent_access)
|
records = base_relation.where(@query_string, @filter_values.with_indifferent_access)
|
||||||
records.any?
|
records.any?
|
||||||
end
|
end
|
||||||
|
|
||||||
def message_conditions
|
def apply_filter(query_hash, current_index)
|
||||||
message_filters = @filters['messages']
|
conversation_filter = @conversation_filters[query_hash['attribute_key']]
|
||||||
|
contact_filter = @contact_filters[query_hash['attribute_key']]
|
||||||
|
message_filter = @message_filters[query_hash['attribute_key']]
|
||||||
|
|
||||||
@rule.conditions.each_with_index do |query_hash, current_index|
|
if conversation_filter
|
||||||
current_filter = message_filters[query_hash['attribute_key']]
|
@query_string += conversation_query_string('conversations', conversation_filter, query_hash.with_indifferent_access, current_index)
|
||||||
@query_string += message_query_string(current_filter, query_hash.with_indifferent_access, current_index)
|
elsif contact_filter
|
||||||
|
@query_string += contact_query_string(contact_filter, query_hash.with_indifferent_access, current_index)
|
||||||
|
elsif message_filter
|
||||||
|
@query_string += message_query_string(message_filter, query_hash.with_indifferent_access, current_index)
|
||||||
|
elsif custom_attribute(query_hash['attribute_key'], @account)
|
||||||
|
# send table name according to attribute key right now we are supporting contact based custom attribute filter
|
||||||
|
@query_string += custom_attribute_query(query_hash.with_indifferent_access, 'contacts', current_index)
|
||||||
end
|
end
|
||||||
records = Message.where(id: @options[:message].id).where(@query_string, @filter_values.with_indifferent_access)
|
|
||||||
records.any?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def message_query_string(current_filter, query_hash, current_index)
|
def message_query_string(current_filter, query_hash, current_index)
|
||||||
|
@ -59,16 +56,18 @@ class AutomationRules::ConditionsFilterService < FilterService
|
||||||
end
|
end
|
||||||
|
|
||||||
# This will be used in future for contact automation rule
|
# This will be used in future for contact automation rule
|
||||||
def contact_conditions(_contact)
|
def contact_query_string(current_filter, query_hash, current_index)
|
||||||
conversation_filters = @filters['conversations']
|
attribute_key = query_hash['attribute_key']
|
||||||
|
query_operator = query_hash['query_operator']
|
||||||
|
|
||||||
@rule.conditions.each_with_index do |query_hash, current_index|
|
filter_operator_value = filter_operation(query_hash, current_index)
|
||||||
current_filter = conversation_filters[query_hash['attribute_key']]
|
|
||||||
@query_string += conversation_query_string(current_filter, query_hash.with_indifferent_access, current_index)
|
case current_filter['attribute_type']
|
||||||
|
when 'additional_attributes'
|
||||||
|
" contacts.additional_attributes ->> '#{attribute_key}' #{filter_operator_value} #{query_operator} "
|
||||||
|
when 'standard'
|
||||||
|
" contacts.#{attribute_key} #{filter_operator_value} #{query_operator} "
|
||||||
end
|
end
|
||||||
|
|
||||||
records = base_relation.where(@query_string, @filter_values.with_indifferent_access)
|
|
||||||
records.any?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def conversation_query_string(table_name, current_filter, query_hash, current_index)
|
def conversation_query_string(table_name, current_filter, query_hash, current_index)
|
||||||
|
@ -91,6 +90,12 @@ class AutomationRules::ConditionsFilterService < FilterService
|
||||||
private
|
private
|
||||||
|
|
||||||
def base_relation
|
def base_relation
|
||||||
Conversation.where(id: @conversation.id).joins('LEFT OUTER JOIN contacts on conversations.contact_id = contacts.id')
|
records = Conversation.where(id: @conversation.id).joins(
|
||||||
|
'LEFT OUTER JOIN contacts on conversations.contact_id = contacts.id'
|
||||||
|
).joins(
|
||||||
|
'LEFT OUTER JOIN messages on messages.conversation_id = conversations.id'
|
||||||
|
)
|
||||||
|
records = records.where(messages: { id: @options[:message].id }) if @options[:message].present?
|
||||||
|
records
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,9 +10,6 @@ describe AutomationRuleListener do
|
||||||
let(:team) { create(:team, account: account) }
|
let(:team) { create(:team, account: account) }
|
||||||
let(:user_1) { create(:user, role: 0) }
|
let(:user_1) { create(:user, role: 0) }
|
||||||
let(:user_2) { create(:user, role: 0) }
|
let(:user_2) { create(:user, role: 0) }
|
||||||
let!(:event) do
|
|
||||||
Events::Base.new('conversation_status_changed', Time.zone.now, { conversation: conversation })
|
|
||||||
end
|
|
||||||
|
|
||||||
before do
|
before do
|
||||||
create(:custom_attribute_definition,
|
create(:custom_attribute_definition,
|
||||||
|
@ -39,7 +36,7 @@ describe AutomationRuleListener do
|
||||||
{ 'action_name' => 'add_label', 'action_params' => %w[support priority_customer] },
|
{ 'action_name' => 'add_label', 'action_params' => %w[support priority_customer] },
|
||||||
{ 'action_name' => 'send_webhook_event', 'action_params' => ['https://www.example.com'] },
|
{ 'action_name' => 'send_webhook_event', 'action_params' => ['https://www.example.com'] },
|
||||||
{ 'action_name' => 'assign_best_agent', 'action_params' => [user_1.id] },
|
{ 'action_name' => 'assign_best_agent', 'action_params' => [user_1.id] },
|
||||||
{ 'action_name' => 'send_email_transcript', 'action_params' => 'new_agent@example.com' },
|
{ 'action_name' => 'send_email_transcript', 'action_params' => ['new_agent@example.com'] },
|
||||||
{ 'action_name' => 'mute_conversation', 'action_params' => nil },
|
{ 'action_name' => 'mute_conversation', 'action_params' => nil },
|
||||||
{ 'action_name' => 'change_status', 'action_params' => ['snoozed'] },
|
{ 'action_name' => 'change_status', 'action_params' => ['snoozed'] },
|
||||||
{ 'action_name' => 'send_message', 'action_params' => ['Send this message.'] },
|
{ 'action_name' => 'send_message', 'action_params' => ['Send this message.'] },
|
||||||
|
@ -70,6 +67,12 @@ describe AutomationRuleListener do
|
||||||
attribute_key: 'customer_type',
|
attribute_key: 'customer_type',
|
||||||
filter_operator: 'equal_to',
|
filter_operator: 'equal_to',
|
||||||
values: ['platinum'],
|
values: ['platinum'],
|
||||||
|
query_operator: 'AND'
|
||||||
|
}.with_indifferent_access,
|
||||||
|
{
|
||||||
|
attribute_key: 'inbox_id',
|
||||||
|
filter_operator: 'equal_to',
|
||||||
|
values: [inbox.id],
|
||||||
query_operator: nil
|
query_operator: nil
|
||||||
}.with_indifferent_access
|
}.with_indifferent_access
|
||||||
]
|
]
|
||||||
|
@ -150,6 +153,40 @@ describe AutomationRuleListener do
|
||||||
expect(conversation.team_id).to eq(team.id)
|
expect(conversation.team_id).to eq(team.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when inbox condition does not match' do
|
||||||
|
let!(:inbox_1) { create(:inbox, account: account) }
|
||||||
|
let!(:event) do
|
||||||
|
Events::Base.new('conversation_updated', Time.zone.now, { conversation: conversation })
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
automation_rule.update!(
|
||||||
|
event_name: 'conversation_updated',
|
||||||
|
name: 'Call actions conversation updated',
|
||||||
|
description: 'Add labels, assign team after conversation updated',
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
attribute_key: 'inbox_id',
|
||||||
|
filter_operator: 'equal_to',
|
||||||
|
values: [inbox_1.id],
|
||||||
|
query_operator: nil
|
||||||
|
}.with_indifferent_access
|
||||||
|
]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'triggers automation rule would not send message to the contacts' do
|
||||||
|
expect(conversation.messages.count).to eq(0)
|
||||||
|
expect(conversation.messages).to be_empty
|
||||||
|
|
||||||
|
listener.conversation_updated(event)
|
||||||
|
|
||||||
|
conversation.reload
|
||||||
|
|
||||||
|
expect(conversation.messages.count).to eq(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#conversation_updated' do
|
describe '#conversation_updated' do
|
||||||
|
@ -305,4 +342,154 @@ describe AutomationRuleListener do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#message_created with conversation and contacts based conditions' do
|
||||||
|
before do
|
||||||
|
automation_rule.update!(
|
||||||
|
event_name: 'message_created',
|
||||||
|
name: 'Call actions message created',
|
||||||
|
description: 'Send Message in the conversation',
|
||||||
|
conditions: [
|
||||||
|
{ attribute_key: 'team_id', filter_operator: 'equal_to', values: [team.id], query_operator: 'AND' }.with_indifferent_access,
|
||||||
|
{ attribute_key: 'message_type', filter_operator: 'equal_to', values: ['incoming'], query_operator: 'AND' }.with_indifferent_access,
|
||||||
|
{ attribute_key: 'email', filter_operator: 'contains', values: ['example.com'], query_operator: 'AND' }.with_indifferent_access,
|
||||||
|
{ attribute_key: 'company', filter_operator: 'equal_to', values: ['Marvel'], query_operator: nil }.with_indifferent_access
|
||||||
|
],
|
||||||
|
actions: [
|
||||||
|
{ 'action_name' => 'send_message', 'action_params' => ['Send this message.'] },
|
||||||
|
{ 'action_name' => 'send_email_transcript', 'action_params' => ['new_agent@example.com'] }
|
||||||
|
]
|
||||||
|
)
|
||||||
|
conversation.update!(team_id: team.id)
|
||||||
|
conversation.contact.update!(email: 'tj@example.com', additional_attributes: { 'company': 'Marvel' })
|
||||||
|
end
|
||||||
|
|
||||||
|
let!(:message) { create(:message, account: account, conversation: conversation, message_type: 'incoming') }
|
||||||
|
let!(:event) do
|
||||||
|
Events::Base.new('message_created', Time.zone.now, { conversation: conversation, message: message })
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when rule matches' do
|
||||||
|
it 'triggers automation rule send email transcript to the mentioned email' do
|
||||||
|
mailer = double
|
||||||
|
|
||||||
|
automation_rule
|
||||||
|
|
||||||
|
listener.message_created(event)
|
||||||
|
|
||||||
|
conversation.reload
|
||||||
|
|
||||||
|
allow(mailer).to receive(:conversation_transcript)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'triggers automation rule send message to the contacts' do
|
||||||
|
expect(conversation.messages.count).to eq(1)
|
||||||
|
|
||||||
|
listener.message_created(event)
|
||||||
|
|
||||||
|
conversation.reload
|
||||||
|
|
||||||
|
expect(conversation.messages.count).to eq(2)
|
||||||
|
expect(conversation.messages.last.content).to eq('Send this message.')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when rule does not match' do
|
||||||
|
before do
|
||||||
|
conversation.update!(team_id: team.id)
|
||||||
|
conversation.contact.update!(email: 'tj@ex.com', additional_attributes: { 'company': 'DC' })
|
||||||
|
end
|
||||||
|
|
||||||
|
let!(:message) { create(:message, account: account, conversation: conversation, message_type: 'outgoing') }
|
||||||
|
let!(:event) do
|
||||||
|
Events::Base.new('message_created', Time.zone.now, { conversation: conversation, message: message })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'triggers automation rule but wont send message' do
|
||||||
|
expect(conversation.messages.count).to eq(1)
|
||||||
|
|
||||||
|
listener.message_created(event)
|
||||||
|
|
||||||
|
conversation.reload
|
||||||
|
|
||||||
|
expect(conversation.messages.count).to eq(1)
|
||||||
|
expect(conversation.messages.last.content).to eq('Incoming Message')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#conversation_created with contacts based conditions' do
|
||||||
|
before do
|
||||||
|
automation_rule.update!(
|
||||||
|
event_name: 'conversation_created',
|
||||||
|
name: 'Call actions message created',
|
||||||
|
description: 'Send Message in the conversation',
|
||||||
|
conditions: [
|
||||||
|
{ attribute_key: 'team_id', filter_operator: 'equal_to', values: [team.id], query_operator: 'AND' }.with_indifferent_access,
|
||||||
|
{ attribute_key: 'email', filter_operator: 'contains', values: ['example.com'], query_operator: 'AND' }.with_indifferent_access,
|
||||||
|
{ attribute_key: 'company', filter_operator: 'equal_to', values: ['Marvel'], query_operator: nil }.with_indifferent_access
|
||||||
|
],
|
||||||
|
actions: [
|
||||||
|
{ 'action_name' => 'send_message', 'action_params' => ['Send this message.'] },
|
||||||
|
{ 'action_name' => 'send_email_transcript', 'action_params' => ['new_agent@example.com'] }
|
||||||
|
]
|
||||||
|
)
|
||||||
|
conversation.update!(team_id: team.id)
|
||||||
|
conversation.contact.update!(email: 'tj@example.com', additional_attributes: { 'company': 'Marvel' })
|
||||||
|
end
|
||||||
|
|
||||||
|
let!(:message) { create(:message, account: account, conversation: conversation, message_type: 'incoming') }
|
||||||
|
let!(:event) do
|
||||||
|
Events::Base.new('conversation_created', Time.zone.now, { conversation: conversation, message: message })
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when rule matches' do
|
||||||
|
it 'triggers automation rule send email transcript to the mentioned email' do
|
||||||
|
mailer = double
|
||||||
|
|
||||||
|
automation_rule
|
||||||
|
|
||||||
|
listener.conversation_created(event)
|
||||||
|
|
||||||
|
conversation.reload
|
||||||
|
|
||||||
|
allow(mailer).to receive(:conversation_transcript)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'triggers automation rule send message to the contacts' do
|
||||||
|
expect(conversation.messages.count).to eq(1)
|
||||||
|
|
||||||
|
listener.conversation_created(event)
|
||||||
|
|
||||||
|
conversation.reload
|
||||||
|
|
||||||
|
expect(conversation.messages.count).to eq(2)
|
||||||
|
expect(conversation.messages.last.content).to eq('Send this message.')
|
||||||
|
expect(conversation.messages.last.content_attributes[:automation_rule_id]).to eq(automation_rule.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when rule does not match' do
|
||||||
|
before do
|
||||||
|
conversation.update!(team_id: team.id)
|
||||||
|
conversation.contact.update!(email: 'tj@ex.com', additional_attributes: { 'company': 'DC' })
|
||||||
|
end
|
||||||
|
|
||||||
|
let!(:message) { create(:message, account: account, conversation: conversation, message_type: 'outgoing') }
|
||||||
|
let!(:event) do
|
||||||
|
Events::Base.new('conversation_created', Time.zone.now, { conversation: conversation, message: message })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'triggers automation rule but wont send message' do
|
||||||
|
expect(conversation.messages.count).to eq(1)
|
||||||
|
|
||||||
|
listener.conversation_created(event)
|
||||||
|
|
||||||
|
conversation.reload
|
||||||
|
|
||||||
|
expect(conversation.messages.count).to eq(1)
|
||||||
|
expect(conversation.messages.last.content).to eq('Incoming Message')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue