mirror of
https://github.com/chatwoot/chatwoot.git
synced 2026-06-04 21:02:35 +08:00
Some checks failed
Frontend Lint & Test / test (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Run Chatwoot CE spec / lint-backend (push) Has been cancelled
Run Chatwoot CE spec / lint-frontend (push) Has been cancelled
Run Chatwoot CE spec / frontend-tests (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (0, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (1, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (10, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (11, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (12, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (13, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (14, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (15, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (2, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (3, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (4, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (5, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (6, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (7, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (8, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (9, 16) (push) Has been cancelled
Publish Chatwoot EE docker images / merge (push) Has been cancelled
Publish Chatwoot CE docker images / merge (push) Has been cancelled
# Pull Request Template ## Description This is the second PR in a series of PRs for Introducing unread counts in the sidebar for inboxes and labels. In this PR: * added api for unread counts * Added the store refresher and invalidation with event listeners * Added action cable event * Added specs for the changes Issue: https://linear.app/chatwoot/issue/CW-6851/support-unread-conversation-counts ## Type of change Please delete options that are not relevant. - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality not to work as expected) - [ ] This change requires a documentation update ## How Has This Been Tested? Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Sojan Jose <sojan@pepalo.com>
1173 lines
46 KiB
Ruby
1173 lines
46 KiB
Ruby
# 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 '#bot_handoff!' do
|
|
let(:conversation) { create(:conversation, status: :pending) }
|
|
|
|
before do
|
|
allow(Rails.configuration.dispatcher).to receive(:dispatch)
|
|
end
|
|
|
|
context 'when waiting_since is blank' do
|
|
before { conversation.update(waiting_since: nil) }
|
|
|
|
it 'sets waiting_since to current time' do
|
|
freeze_time do
|
|
conversation.bot_handoff!
|
|
expect(conversation.reload.waiting_since).to eq(Time.current)
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when waiting_since is already set' do
|
|
let(:original_time) { 1.hour.ago }
|
|
|
|
before { conversation.update(waiting_since: original_time) }
|
|
|
|
it 'preserves existing waiting_since' do
|
|
conversation.bot_handoff!
|
|
expect(conversation.reload.waiting_since).to be_within(1.second).of(original_time)
|
|
end
|
|
end
|
|
|
|
it 'changes status to open' do
|
|
conversation.bot_handoff!
|
|
expect(conversation.reload.status).to eq('open')
|
|
end
|
|
|
|
it 'dispatches CONVERSATION_BOT_HANDOFF event' do
|
|
expect(Rails.configuration.dispatcher).to receive(:dispatch)
|
|
.with(described_class::CONVERSATION_BOT_HANDOFF, anything, hash_including(conversation: conversation))
|
|
conversation.bot_handoff!
|
|
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
|
|
|
|
it 'dispatches conversation deleted event with unread count cache data' do
|
|
allow(Rails.configuration.dispatcher).to receive(:dispatch)
|
|
|
|
conversation.destroy!
|
|
|
|
expect(Rails.configuration.dispatcher).to have_received(:dispatch).with(
|
|
'conversation.deleted',
|
|
kind_of(Time),
|
|
conversation_data: {
|
|
id: conversation.id,
|
|
account_id: conversation.account_id,
|
|
inbox_id: conversation.inbox_id,
|
|
assignee_id: conversation.assignee_id,
|
|
team_id: conversation.team_id,
|
|
cached_label_list: conversation.cached_label_list
|
|
}
|
|
)
|
|
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
|
|
|
|
context 'when some conversations have a null waiting_since' do
|
|
before do
|
|
# rubocop:disable Rails/SkipsModelValidations
|
|
conversation_5.update_column(:waiting_since, nil)
|
|
conversation_2.update_column(:waiting_since, nil)
|
|
# rubocop:enable Rails/SkipsModelValidations
|
|
end
|
|
|
|
it 'places null waiting_since conversations at the end in ascending order' do
|
|
records = described_class.sort_on_waiting_since
|
|
expect(records.map(&:id)).to eq [
|
|
conversation_4.id, conversation_6.id, conversation_7.id, conversation_3.id, conversation_1.id,
|
|
conversation_5.id, conversation_2.id
|
|
]
|
|
end
|
|
|
|
it 'places null waiting_since conversations at the end in descending order' do
|
|
records = described_class.sort_on_waiting_since(:desc)
|
|
expect(records.map(&:id)).to eq [
|
|
conversation_1.id, conversation_3.id, conversation_7.id, conversation_6.id, conversation_4.id,
|
|
conversation_5.id, conversation_2.id
|
|
]
|
|
end
|
|
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
|