# frozen_string_literal: true require 'rails_helper' require Rails.root.join 'spec/models/concerns/assignment_handler_shared.rb' require Rails.root.join 'spec/models/concerns/auto_assignment_handler_shared.rb' RSpec.describe Conversation do after do Current.user = nil Current.account = nil end describe 'associations' do it { is_expected.to belong_to(:account) } it { is_expected.to belong_to(:inbox) } it { is_expected.to belong_to(:contact) } it { is_expected.to belong_to(:contact_inbox) } it { is_expected.to belong_to(:assignee).optional } it { is_expected.to belong_to(:team).optional } it { is_expected.to belong_to(:campaign).optional } end describe 'concerns' do it_behaves_like 'assignment_handler' it_behaves_like 'auto_assignment_handler' end describe '.before_create' do let(:conversation) { build(:conversation, display_id: nil) } before do conversation.save! conversation.reload end it 'runs before_create callbacks' do expect(conversation.display_id).to eq(1) end it 'sets waiting since' do expect(conversation.waiting_since).not_to be_nil end it 'creates a UUID for every conversation automatically' do uuid_pattern = /[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}$/i expect(conversation.uuid).to match(uuid_pattern) end end describe '.after_create' do let(:account) { create(:account) } let(:agent) { create(:user, email: 'agent1@example.com', account: account) } let(:inbox) { create(:inbox, account: account) } let(:conversation) do create( :conversation, account: account, contact: create(:contact, account: account), inbox: inbox, assignee: nil ) end before do allow(Rails.configuration.dispatcher).to receive(:dispatch) end it 'runs after_create callbacks' do # send_events expect(Rails.configuration.dispatcher).to have_received(:dispatch) .with(described_class::CONVERSATION_CREATED, kind_of(Time), conversation: conversation, notifiable_assignee_change: false, changed_attributes: nil, performed_by: nil) end end describe '.validate jsonb attributes' do let(:account) { create(:account) } let(:agent) { create(:user, email: 'agent1@example.com', account: account) } let(:inbox) { create(:inbox, account: account) } let(:conversation) do create( :conversation, account: account, contact: create(:contact, account: account), inbox: inbox, assignee: nil ) end it 'validate length of additional_attributes value' do conversation.additional_attributes = { company_name: 'some_company' * 200, contact_number: 19_999_999_999 } conversation.valid? error_messages = conversation.errors.messages expect(error_messages[:additional_attributes][0]).to eq('company_name length should be < 1500') expect(error_messages[:additional_attributes][1]).to eq('contact_number value should be < 9999999999') end it 'validate length of custom_attributes value' do conversation.custom_attributes = { company_name: 'some_company' * 200, contact_number: 19_999_999_999 } conversation.valid? error_messages = conversation.errors.messages expect(error_messages[:custom_attributes][0]).to eq('company_name length should be < 1500') expect(error_messages[:custom_attributes][1]).to eq('contact_number value should be < 9999999999') end end describe '.after_update' do let!(:account) { create(:account) } let!(:old_assignee) do create(:user, email: 'agent1@example.com', account: account, role: :agent) end let(:new_assignee) do create(:user, email: 'agent2@example.com', account: account, role: :agent) end let!(:conversation) do create(:conversation, status: 'open', account: account, assignee: old_assignee) end let(:assignment_mailer) { instance_double(AssignmentMailer, deliver: true) } let(:label) { create(:label, account: account) } before do create(:inbox_member, user: old_assignee, inbox: conversation.inbox) create(:inbox_member, user: new_assignee, inbox: conversation.inbox) allow(Rails.configuration.dispatcher).to receive(:dispatch) Current.user = old_assignee end it 'sends conversation updated event if labels are updated' do conversation.update(label_list: [label.title]) changed_attributes = conversation.previous_changes expect(Rails.configuration.dispatcher).to have_received(:dispatch) .with( described_class::CONVERSATION_UPDATED, kind_of(Time), conversation: conversation, notifiable_assignee_change: false, changed_attributes: changed_attributes, performed_by: nil ) end it 'runs after_update callbacks' do conversation.update( status: :resolved, contact_last_seen_at: Time.zone.now, assignee: new_assignee ) status_change = conversation.status_change changed_attributes = conversation.previous_changes expect(Rails.configuration.dispatcher).to have_received(:dispatch) .with(described_class::CONVERSATION_RESOLVED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true, changed_attributes: status_change, performed_by: nil) expect(Rails.configuration.dispatcher).to have_received(:dispatch) .with(described_class::CONVERSATION_READ, kind_of(Time), conversation: conversation, notifiable_assignee_change: true, changed_attributes: nil, performed_by: nil) expect(Rails.configuration.dispatcher).to have_received(:dispatch) .with(described_class::ASSIGNEE_CHANGED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true, changed_attributes: changed_attributes, performed_by: nil) expect(Rails.configuration.dispatcher).to have_received(:dispatch) .with(described_class::CONVERSATION_UPDATED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true, changed_attributes: changed_attributes, performed_by: nil) end it 'will not run conversation_updated event for empty updates' do conversation.save! expect(Rails.configuration.dispatcher).not_to have_received(:dispatch) .with(described_class::CONVERSATION_UPDATED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true) end it 'will not run conversation_updated event for non whitelisted keys' do conversation.update(updated_at: DateTime.now.utc) expect(Rails.configuration.dispatcher).not_to have_received(:dispatch) .with(described_class::CONVERSATION_UPDATED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true) end it 'will run conversation_updated event for conversation_language in additional_attributes' do conversation.additional_attributes[:conversation_language] = 'es' conversation.save! changed_attributes = conversation.previous_changes expect(Rails.configuration.dispatcher).to have_received(:dispatch) .with(described_class::CONVERSATION_UPDATED, kind_of(Time), conversation: conversation, notifiable_assignee_change: false, changed_attributes: changed_attributes, performed_by: nil) end it 'will not run conversation_updated event for bowser_language in additional_attributes' do conversation.additional_attributes[:browser_language] = 'es' conversation.save! expect(Rails.configuration.dispatcher).not_to have_received(:dispatch) .with(described_class::CONVERSATION_UPDATED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true) end it 'creates conversation activities' do conversation.update( status: :resolved, contact_last_seen_at: Time.zone.now, assignee: new_assignee, label_list: [label.title] ) expect(Conversations::ActivityMessageJob) .to(have_been_enqueued.at_least(:once) .with(conversation, { account_id: conversation.account_id, inbox_id: conversation.inbox_id, message_type: :activity, content: "#{old_assignee.name} added #{label.title}" })) expect(Conversations::ActivityMessageJob) .to(have_been_enqueued.at_least(:once) .with(conversation, { account_id: conversation.account_id, inbox_id: conversation.inbox_id, message_type: :activity, content: "Conversation was marked resolved by #{old_assignee.name}" })) expect(Conversations::ActivityMessageJob) .to(have_been_enqueued.at_least(:once) .with(conversation, { account_id: conversation.account_id, inbox_id: conversation.inbox_id, message_type: :activity, content: "Assigned to #{new_assignee.name} by #{old_assignee.name}" })) end it 'adds a message for system auto resolution if marked resolved by system' do account.update(auto_resolve_after: 40 * 24 * 60) conversation2 = create(:conversation, status: 'open', account: account, assignee: old_assignee) Current.reset message_data = if account.auto_resolve_after >= 1440 && account.auto_resolve_after % 1440 == 0 { key: 'auto_resolved_days', count: account.auto_resolve_after / 1440 } elsif account.auto_resolve_after >= 60 && account.auto_resolve_after % 60 == 0 { key: 'auto_resolved_hours', count: account.auto_resolve_after / 60 } else { key: 'auto_resolved_minutes', count: account.auto_resolve_after } end system_resolved_message = "Conversation was marked resolved by system due to #{message_data[:count]} days of inactivity" expect { conversation2.update(status: :resolved) } .to have_enqueued_job(Conversations::ActivityMessageJob) .with(conversation2, { account_id: conversation2.account_id, inbox_id: conversation2.inbox_id, message_type: :activity, content: system_resolved_message }) end end describe '#update_labels' do let(:account) { create(:account) } let(:conversation) { create(:conversation, account: account) } let(:agent) do create(:user, email: 'agent@example.com', account: account, role: :agent) end let(:first_label) { create(:label, account: account) } let(:second_label) { create(:label, account: account) } let(:third_label) { create(:label, account: account) } let(:fourth_label) { create(:label, account: account) } before do conversation Current.user = agent first_label second_label third_label fourth_label end it 'adds one label to conversation' do labels = [first_label].map(&:title) expect { conversation.update_labels(labels) } .to have_enqueued_job(Conversations::ActivityMessageJob) .with(conversation, { account_id: conversation.account_id, inbox_id: conversation.inbox_id, message_type: :activity, content: "#{agent.name} added #{labels.join(', ')}" }) expect(conversation.label_list).to match_array(labels) end it 'adds and removes previously added labels' do labels = [first_label, fourth_label].map(&:title) expect { conversation.update_labels(labels) } .to have_enqueued_job(Conversations::ActivityMessageJob) .with(conversation, { account_id: conversation.account_id, inbox_id: conversation.inbox_id, message_type: :activity, content: "#{agent.name} added #{labels.join(', ')}" }) expect(conversation.label_list).to match_array(labels) updated_labels = [second_label, third_label].map(&:title) expect(conversation.update_labels(updated_labels)).to be(true) expect(conversation.label_list).to match_array(updated_labels) expect(Conversations::ActivityMessageJob) .to(have_been_enqueued.at_least(:once) .with(conversation, { account_id: conversation.account_id, inbox_id: conversation.inbox_id, message_type: :activity, content: "#{agent.name} added #{updated_labels.join(', ')}" })) expect(Conversations::ActivityMessageJob) .to(have_been_enqueued.at_least(:once) .with(conversation, { account_id: conversation.account_id, inbox_id: conversation.inbox_id, message_type: :activity, content: "#{agent.name} removed #{labels.join(', ')}" })) end end describe '#toggle_status' do it 'toggles conversation status to resolved when open' do conversation = create(:conversation, status: 'open') expect(conversation.toggle_status).to be(true) expect(conversation.reload.status).to eq('resolved') end it 'toggles conversation status to open when resolved' do conversation = create(:conversation, status: 'resolved') expect(conversation.toggle_status).to be(true) expect(conversation.reload.status).to eq('open') end it 'toggles conversation status to open when pending' do conversation = create(:conversation, status: 'pending') expect(conversation.toggle_status).to be(true) expect(conversation.reload.status).to eq('open') end it 'toggles conversation status to open when snoozed' do conversation = create(:conversation, status: 'snoozed') expect(conversation.toggle_status).to be(true) expect(conversation.reload.status).to eq('open') end end describe '#toggle_priority' do it 'defaults priority to nil when created' do conversation = create(:conversation, status: 'open') expect(conversation.priority).to be_nil end it 'toggles the priority to nil if nothing is passed' do conversation = create(:conversation, status: 'open', priority: 'high') expect(conversation.toggle_priority).to be(true) expect(conversation.reload.priority).to be_nil end it 'sets the priority to low' do conversation = create(:conversation, status: 'open') expect(conversation.toggle_priority('low')).to be(true) expect(conversation.reload.priority).to eq('low') end it 'sets the priority to medium' do conversation = create(:conversation, status: 'open') expect(conversation.toggle_priority('medium')).to be(true) expect(conversation.reload.priority).to eq('medium') end it 'sets the priority to high' do conversation = create(:conversation, status: 'open') expect(conversation.toggle_priority('high')).to be(true) expect(conversation.reload.priority).to eq('high') end it 'sets the priority to urgent' do conversation = create(:conversation, status: 'open') expect(conversation.toggle_priority('urgent')).to be(true) expect(conversation.reload.priority).to eq('urgent') end end describe '#ensure_snooze_until_reset' do it 'resets the snoozed_until when status is toggled' do conversation = create(:conversation, status: 'snoozed', snoozed_until: 2.days.from_now) expect(conversation.snoozed_until).not_to be_nil expect(conversation.toggle_status).to be(true) expect(conversation.reload.snoozed_until).to be_nil end end describe '#mute!' do subject(:mute!) { conversation.mute! } let(:user) do create(:user, email: 'agent2@example.com', account: create(:account), role: :agent) end let(:conversation) { create(:conversation) } before { Current.user = user } it 'marks conversation as resolved' do mute! expect(conversation.reload.resolved?).to be(true) end it 'blocks the contact' do mute! expect(conversation.reload.contact.blocked?).to be(true) end it 'creates mute message' do mute! expect(Conversations::ActivityMessageJob) .to(have_been_enqueued.at_least(:once).with(conversation, { account_id: conversation.account_id, inbox_id: conversation.inbox_id, message_type: :activity, content: "#{user.name} has muted the conversation" })) end context 'when contact is missing' do before do conversation.update_columns(contact_id: nil, contact_inbox_id: nil) # rubocop:disable Rails/SkipsModelValidations end it 'does not change conversation status' do expect { mute! }.not_to(change { conversation.reload.status }) end it 'does not enqueue an activity message' do expect { mute! }.not_to have_enqueued_job(Conversations::ActivityMessageJob) end end end describe '#unmute!' do subject(:unmute!) { conversation.unmute! } let(:user) do create(:user, email: 'agent2@example.com', account: create(:account), role: :agent) end let(:conversation) { create(:conversation).tap(&:mute!) } before { Current.user = user } it 'does not change conversation status' do expect { unmute! }.not_to(change { conversation.reload.status }) end it 'unblocks the contact' do unmute! expect(conversation.reload.contact.blocked?).to be(false) end it 'creates unmute message' do unmute! expect(Conversations::ActivityMessageJob) .to(have_been_enqueued.at_least(:once).with(conversation, { account_id: conversation.account_id, inbox_id: conversation.inbox_id, message_type: :activity, content: "#{user.name} has unmuted the conversation" })) end context 'when contact is missing' do let(:conversation) { create(:conversation) } before do conversation.update_columns(contact_id: nil, contact_inbox_id: nil) # rubocop:disable Rails/SkipsModelValidations end it 'does not change conversation status' do expect { unmute! }.not_to(change { conversation.reload.status }) end it 'does not enqueue an activity message' do expect { unmute! }.not_to have_enqueued_job(Conversations::ActivityMessageJob) end end end describe '#muted?' do subject(:muted?) { conversation.muted? } let(:conversation) { create(:conversation) } it 'return true if conversation is muted' do conversation.mute! expect(muted?).to be(true) end it 'returns false if conversation is not muted' do expect(muted?).to be(false) end context 'when contact is missing' do before do conversation.update_columns(contact_id: nil, contact_inbox_id: nil) # rubocop:disable Rails/SkipsModelValidations end it 'returns false' do expect(muted?).to be(false) end end end describe 'unread_messages' do subject(:unread_messages) { conversation.unread_messages } let(:conversation) { create(:conversation, agent_last_seen_at: 1.hour.ago) } let(:message_params) do { conversation: conversation, account: conversation.account, inbox: conversation.inbox, sender: conversation.assignee } end let!(:message) do create(:message, created_at: 1.minute.ago, **message_params) end before do create(:message, created_at: 1.month.ago, **message_params) end it 'returns unread messages' do expect(unread_messages).to include(message) end end describe 'recent_messages' do subject(:recent_messages) { conversation.recent_messages } let(:conversation) { create(:conversation, agent_last_seen_at: 1.hour.ago) } let(:message_params) do { conversation: conversation, account: conversation.account, inbox: conversation.inbox, sender: conversation.assignee } end let!(:messages) do create_list(:message, 10, **message_params) do |message, i| message.created_at = i.minute.ago end end it 'returns upto 5 recent messages' do expect(recent_messages.length).to be < 6 expect(recent_messages).to eq messages.last(5) end end describe 'unread_incoming_messages' do subject(:unread_incoming_messages) { conversation.unread_incoming_messages } let(:conversation) { create(:conversation, agent_last_seen_at: 1.hour.ago) } let(:message_params) do { conversation: conversation, account: conversation.account, inbox: conversation.inbox, sender: conversation.assignee, created_at: 1.minute.ago } end let!(:message) do create(:message, message_type: :incoming, **message_params) end before do create(:message, message_type: :outgoing, **message_params) end it 'returns unread incoming messages' do expect(unread_incoming_messages).to contain_exactly(message) end it 'returns unread incoming messages even if the agent has not seen the conversation' do conversation.update!(agent_last_seen_at: nil) expect(unread_incoming_messages).to contain_exactly(message) end end describe '#push_event_data' do subject(:push_event_data) { conversation.push_event_data } let(:conversation) { create(:conversation) } let(:expected_data) do { additional_attributes: {}, meta: { sender: conversation.contact.push_event_data, assignee: conversation.assigned_entity&.push_event_data, assignee_type: conversation.assignee_type, team: conversation.team&.push_event_data, hmac_verified: conversation.contact_inbox.hmac_verified }, id: conversation.display_id, messages: [], labels: [], last_activity_at: conversation.last_activity_at.to_i, inbox_id: conversation.inbox_id, status: conversation.status, contact_inbox: conversation.contact_inbox, timestamp: conversation.last_activity_at.to_i, can_reply: true, channel: 'Channel::WebWidget', snoozed_until: conversation.snoozed_until, custom_attributes: conversation.custom_attributes, first_reply_created_at: nil, contact_last_seen_at: conversation.contact_last_seen_at.to_i, agent_last_seen_at: conversation.agent_last_seen_at.to_i, created_at: conversation.created_at.to_i, updated_at: conversation.updated_at.to_f, waiting_since: conversation.waiting_since.to_i, priority: nil, unread_count: 0 } end it 'returns push event payload' do expect(push_event_data).to eq(expected_data) end end describe 'when conversation is created by blocked contact' do let(:account) { create(:account) } let(:blocked_contact) { create(:contact, account: account, blocked: true) } let(:inbox) { create(:inbox, account: account) } it 'creates conversation in resolved state' do conversation = create(:conversation, account: account, contact: blocked_contact, inbox: inbox) expect(conversation.status).to eq('resolved') end end describe '#botinbox: when conversation created inside inbox with agent bot' do let!(:bot_inbox) { create(:agent_bot_inbox) } let(:conversation) { create(:conversation, inbox: bot_inbox.inbox) } it 'returns conversation status as pending' do expect(conversation.status).to eq('pending') end context 'with campaigns' do let(:user) { create(:user, account: bot_inbox.inbox.account) } it 'returns conversation as open if campaign has a sender' do campaign = create(:campaign, inbox: bot_inbox.inbox, account: bot_inbox.inbox.account, sender: user) conversation = create(:conversation, inbox: bot_inbox.inbox, campaign: campaign) expect(conversation.status).to eq('open') end it 'returns conversation as pending if campaign has no sender (bot-initiated) and bot is active' do campaign = create(:campaign, inbox: bot_inbox.inbox, account: bot_inbox.inbox.account, sender: nil) conversation = create(:conversation, inbox: bot_inbox.inbox, campaign: campaign) expect(conversation.status).to eq('pending') end end context 'with campaigns in inbox without bot' do let(:account) { create(:account) } let(:inbox) { create(:inbox, account: account) } let(:user) { create(:user, account: account) } it 'returns conversation as open if campaign has no sender but no bot is active' do campaign = create(:campaign, inbox: inbox, account: account, sender: nil) conversation = create(:conversation, inbox: inbox, campaign: campaign) expect(conversation.status).to eq('open') end it 'returns conversation as open if campaign has a sender' do campaign = create(:campaign, inbox: inbox, account: account, sender: user) conversation = create(:conversation, inbox: inbox, campaign: campaign) expect(conversation.status).to eq('open') end end end describe '#botintegration: when conversation created in inbox with dialogflow integration' do let(:inbox) { create(:inbox) } let(:hook) { create(:integrations_hook, :dialogflow, inbox: inbox) } let(:conversation) { create(:conversation, inbox: hook.inbox) } it 'returns conversation status as pending' do expect(conversation.status).to eq('pending') end end describe '#delete conversation' do include ActiveJob::TestHelper let!(:conversation) { create(:conversation) } let!(:notification) { create(:notification, notification_type: 'conversation_creation', primary_actor: conversation) } it 'delete associated notifications if conversation is deleted' do perform_enqueued_jobs do conversation.destroy! end expect { notification.reload }.to raise_error ActiveRecord::RecordNotFound end end describe 'validate invalid referer url' do let(:conversation) { create(:conversation, additional_attributes: { referer: 'javascript' }) } it 'returns nil' do expect(conversation['additional_attributes']['referer']).to be_nil end end describe 'validate valid referer url' do let(:conversation) { create(:conversation, additional_attributes: { referer: 'https://www.chatwoot.com/' }) } it 'returns nil' do expect(conversation['additional_attributes']['referer']).to eq('https://www.chatwoot.com/') end end describe 'custom sort option' do include ActiveJob::TestHelper let!(:conversation_7) { create(:conversation, created_at: DateTime.now - 6.days, last_activity_at: DateTime.now - 13.days) } let!(:conversation_6) { create(:conversation, created_at: DateTime.now - 7.days, last_activity_at: DateTime.now - 10.days) } let!(:conversation_5) { create(:conversation, created_at: DateTime.now - 8.days, last_activity_at: DateTime.now - 12.days, priority: :urgent) } let!(:conversation_4) { create(:conversation, created_at: DateTime.now - 9.days, last_activity_at: DateTime.now - 11.days, priority: :urgent) } let!(:conversation_3) { create(:conversation, created_at: DateTime.now - 5.days, last_activity_at: DateTime.now - 9.days, priority: :low) } let!(:conversation_2) { create(:conversation, created_at: DateTime.now - 3.days, last_activity_at: DateTime.now - 6.days, priority: :high) } let!(:conversation_1) { create(:conversation, created_at: DateTime.now - 4.days, last_activity_at: DateTime.now - 8.days, priority: :medium) } describe 'sort_on_created_at' do let(:created_desc_order) do [ conversation_2.id, conversation_1.id, conversation_3.id, conversation_7.id, conversation_6.id, conversation_5.id, conversation_4.id ] end it 'returns the list in ascending order by default' do records = described_class.sort_on_created_at expect(records.map(&:id)).to eq created_desc_order.reverse end it 'returns the list in descending order if desc is passed as sort direction' do records = described_class.sort_on_created_at(:desc) expect(records.map(&:id)).to eq created_desc_order end end describe 'sort_on_last_activity_at' do let(:last_activity_asc_order) do [ conversation_7.id, conversation_5.id, conversation_4.id, conversation_6.id, conversation_3.id, conversation_1.id, conversation_2.id ] end it 'returns the list in descending order by default' do records = described_class.sort_on_last_activity_at expect(records.map(&:id)).to eq last_activity_asc_order.reverse end it 'returns the list in asc order if asc is passed as sort direction' do records = described_class.sort_on_last_activity_at(:asc) expect(records.map(&:id)).to eq last_activity_asc_order end end context 'when last_activity_at updated by some actions' do before do create(:message, conversation_id: conversation_1.id, message_type: :incoming, created_at: DateTime.now - 8.days) create(:message, conversation_id: conversation_2.id, message_type: :incoming, created_at: DateTime.now - 6.days) create(:message, conversation_id: conversation_3.id, message_type: :incoming, created_at: DateTime.now - 2.days) end it 'sort conversations with latest resolved conversation at first' do records = described_class.sort_on_last_activity_at expect(records.first.id).to eq(conversation_3.id) conversation_1.toggle_status perform_enqueued_jobs do Conversations::ActivityMessageJob.perform_later( conversation_1, account_id: conversation_1.account_id, inbox_id: conversation_1.inbox_id, message_type: :activity, content: 'Conversation was marked resolved by system due to days of inactivity' ) end records = described_class.sort_on_last_activity_at expect(records.first.id).to eq(conversation_1.id) end it 'Sort conversations with latest message' do create(:message, conversation_id: conversation_3.id, message_type: :incoming, created_at: DateTime.now) records = described_class.sort_on_last_activity_at expect(records.first.id).to eq(conversation_3.id) end end describe 'sort_on_priority' do it 'return list with the following order urgent > high > medium > low > nil by default' do # ensure they are not pre-sorted records = described_class.sort_on_created_at expect(records.pluck(:priority)).not_to eq(['urgent', 'urgent', 'high', 'medium', 'low', nil, nil]) records = described_class.sort_on_priority expect(records.pluck(:priority)).to eq(['urgent', 'urgent', 'high', 'medium', 'low', nil, nil]) expect(records.pluck(:id)).to eq( [ conversation_4.id, conversation_5.id, conversation_2.id, conversation_1.id, conversation_3.id, conversation_6.id, conversation_7.id ] ) end it 'return list with the following order low > medium > high > urgent > nil by default' do # ensure they are not pre-sorted records = described_class.sort_on_created_at expect(records.pluck(:priority)).not_to eq(['urgent', 'urgent', 'high', 'medium', 'low', nil, nil]) records = described_class.sort_on_priority(:asc) expect(records.pluck(:priority)).to eq(['low', 'medium', 'high', 'urgent', 'urgent', nil, nil]) expect(records.pluck(:id)).to eq( [ conversation_3.id, conversation_1.id, conversation_2.id, conversation_4.id, conversation_5.id, conversation_6.id, conversation_7.id ] ) end it 'sorts conversation with last_activity for the same priority' do records = described_class.where(priority: 'urgent').sort_on_priority # ensure that the conversation 4 last_activity_at is more recent than conversation 5 expect(conversation_4.last_activity_at > conversation_5.last_activity_at).to be(true) expect(records.pluck(:priority, :id)).to eq([['urgent', conversation_4.id], ['urgent', conversation_5.id]]) records = described_class.where(priority: nil).sort_on_priority # ensure that the conversation 6 last_activity_at is more recent than conversation 7 expect(conversation_6.last_activity_at > conversation_7.last_activity_at).to be(true) expect(records.pluck(:priority, :id)).to eq([[nil, conversation_6.id], [nil, conversation_7.id]]) end end describe 'sort_on_waiting_since' do it 'returns the list in ascending order by default' do records = described_class.sort_on_waiting_since expect(records.map(&:id)).to eq [ conversation_4.id, conversation_5.id, conversation_6.id, conversation_7.id, conversation_3.id, conversation_1.id, conversation_2.id ] end it 'returns the list in desc order if asc is passed as sort direction' do records = described_class.sort_on_waiting_since(:desc) expect(records.map(&:id)).to eq [ conversation_2.id, conversation_1.id, conversation_3.id, conversation_7.id, conversation_6.id, conversation_5.id, conversation_4.id ] end end end describe 'cached_label_list_array' do let(:conversation) { create(:conversation) } it 'returns the correct list of labels' do conversation.update(label_list: %w[customer-support enterprise paid-customer]) expect(conversation.cached_label_list_array).to eq %w[customer-support enterprise paid-customer] end end describe '#last_activity_at' do let(:conversation) { create(:conversation) } let(:message_params) do { conversation: conversation, account: conversation.account, inbox: conversation.inbox, sender: conversation.assignee } end context 'when a new conversation is created' do it 'sets last_activity_at to the created_at time (within DB precision)' do expect(conversation.last_activity_at).to be_within(1.second).of(conversation.created_at) end end context 'when a new message is added' do it 'updates the last_activity_at to the new message\'s created_at time' do message = create(:message, created_at: 1.hour.from_now, **message_params) conversation.reload expect(conversation.last_activity_at).to be_within(1.second).of(message.created_at) end end context 'when multiple messages are added' do it 'sets last_activity_at to the most recent message\'s created_at time' do create(:message, created_at: 2.hours.ago, **message_params) latest_message = create(:message, created_at: 1.hour.from_now, **message_params) conversation.reload expect(conversation.last_activity_at).to be_within(1.second).of(latest_message.created_at) end end end describe '#can_reply?' do let(:conversation) { create(:conversation) } let(:message_window_service) { instance_double(Conversations::MessageWindowService) } before do allow(Conversations::MessageWindowService).to receive(:new).with(conversation).and_return(message_window_service) end it 'delegates to MessageWindowService' do allow(message_window_service).to receive(:can_reply?).and_return(true) expect(conversation.can_reply?).to be true expect(message_window_service).to have_received(:can_reply?) end it 'returns false when MessageWindowService returns false' do allow(message_window_service).to receive(:can_reply?).and_return(false) expect(conversation.can_reply?).to be false expect(message_window_service).to have_received(:can_reply?) end end describe 'reply time calculation flows' do include ActiveJob::TestHelper let(:account) { create(:account) } let(:inbox) { create(:inbox, account: account) } let(:contact) { create(:contact, account: account) } let(:agent) { create(:user, account: account, role: :agent) } let(:conversation) { create(:conversation, account: account, inbox: inbox, contact: contact, assignee: agent, waiting_since: nil) } let(:conversation_start_time) { 5.hours.ago } before do create(:inbox_member, user: agent, inbox: inbox) # rubocop:disable Rails/SkipsModelValidations conversation.update_column(:waiting_since, nil) conversation.update_column(:created_at, conversation_start_time) # rubocop:enable Rails/SkipsModelValidations conversation.messages.destroy_all conversation.reporting_events.destroy_all conversation.reload end def create_customer_message(conversation, created_at: Time.current) message = nil perform_enqueued_jobs do message = create(:message, message_type: 'incoming', account: conversation.account, inbox: conversation.inbox, conversation: conversation, sender: conversation.contact, created_at: created_at) end message end def create_agent_message(conversation, created_at: Time.current) message = nil perform_enqueued_jobs do message = create(:message, message_type: 'outgoing', account: conversation.account, inbox: conversation.inbox, conversation: conversation, sender: conversation.assignee, created_at: created_at) end message end it 'correctly tracks waiting_since and creates first response time events' do create_customer_message(conversation, created_at: conversation_start_time) conversation.reload expect(conversation.waiting_since).to be_within(1.second).of(conversation_start_time) # Agent replies - this should create first response event agent_reply1_time = 4.hours.ago create_agent_message(conversation, created_at: agent_reply1_time) first_response_events = account.reporting_events.where(name: 'first_response', conversation_id: conversation.id) expect(first_response_events.count).to eq(1) expect(first_response_events.first.value).to be_within(1.second).of(1.hour) # the first response should also clear the waiting_since conversation.reload expect(conversation.waiting_since).to be_nil end it 'does not reset waiting_since if customer sends another message' do create_customer_message(conversation, created_at: conversation_start_time) conversation.reload expect(conversation.waiting_since).to be_within(1.second).of(conversation_start_time) create_customer_message(conversation, created_at: 3.hours.ago) conversation.reload expect(conversation.waiting_since).to be_within(1.second).of(conversation_start_time) end it 'records the correct reply_time for subsequent messages' do create_customer_message(conversation, created_at: conversation_start_time) create_agent_message(conversation, created_at: 4.hours.ago) create_customer_message(conversation, created_at: 3.hours.ago) create_agent_message(conversation, created_at: 2.hours.ago) reply_events = account.reporting_events.where(name: 'reply_time', conversation_id: conversation.id) expect(reply_events.count).to eq(1) expect(reply_events.first.value).to be_within(1.second).of(1.hour) conversation.reload expect(conversation.waiting_since).to be_nil end it 'records zero reply time if an agent sends a message after resolution' do create_customer_message(conversation, created_at: conversation_start_time) create_agent_message(conversation, created_at: 4.hours.ago) create_customer_message(conversation, created_at: 3.hours.ago) conversation.toggle_status expect(conversation.status).to eq('resolved') conversation.toggle_status expect(conversation.status).to eq('open') conversation.reload expect(conversation.waiting_since).to be_nil create_agent_message(conversation, created_at: 1.hour.ago) # update_waiting_since will ensure that no events were created since the waiting_since was nil # if the event is created it should log zero value, we have handled that in the reporting_event_listener reply_events = account.reporting_events.where(name: 'reply_time', conversation_id: conversation.id) expect(reply_events.count).to eq(0) end context 'when AgentBot responds between customer messages' do let(:agent_bot) { create(:agent_bot, account: account) } def create_bot_message(conversation, created_at: Time.current) message = nil perform_enqueued_jobs do message = create(:message, message_type: 'outgoing', account: conversation.account, inbox: conversation.inbox, conversation: conversation, sender: agent_bot, created_at: created_at) end message end it 'calculates reply time from the most recent customer message after bot response' do # Initial conversation: customer message -> agent first reply (to establish first_reply_created_at) create_customer_message(conversation, created_at: 10.hours.ago) create_agent_message(conversation, created_at: 9.hours.ago) # Customer message 1 create_customer_message(conversation, created_at: 5.hours.ago) # Bot responds create_bot_message(conversation, created_at: 4.hours.ago) # Customer message 2 (after bot response) - should reset waiting_since create_customer_message(conversation, created_at: 2.hours.ago) # Human agent replies - should create reply_time event from customer message 2 create_agent_message(conversation, created_at: 1.hour.ago) reply_events = account.reporting_events.where(name: 'reply_time', conversation_id: conversation.id) expect(reply_events.count).to eq(1) # Only the second agent reply creates a reply_time event # Reply time should be 1 hour (from customer message 2 to agent reply) expect(reply_events.first.value).to be_within(60).of(3600) end it 'handles multiple bot responses before customer messages again' do # Initial conversation: customer message -> agent first reply create_customer_message(conversation, created_at: 10.hours.ago) create_agent_message(conversation, created_at: 9.hours.ago) # Customer message 1 create_customer_message(conversation, created_at: 6.hours.ago) # Bot responds multiple times create_bot_message(conversation, created_at: 5.hours.ago) create_bot_message(conversation, created_at: 4.hours.ago) # Customer message 2 (after multiple bot responses) - should reset waiting_since create_customer_message(conversation, created_at: 2.hours.ago) # Human agent replies create_agent_message(conversation, created_at: 1.hour.ago) reply_events = account.reporting_events.where(name: 'reply_time', conversation_id: conversation.id) expect(reply_events.count).to eq(1) # Only the second agent reply creates a reply_time event # Reply time should be 1 hour (from customer message 2 to agent reply) expect(reply_events.first.value).to be_within(60).of(3600) end end end end