Restructure omni services and add Chatwoot research snapshot

This commit is contained in:
Ruslan Bakiev
2026-02-21 11:11:27 +07:00
parent edea7a0034
commit b73babbbf6
7732 changed files with 978203 additions and 32 deletions

View File

@@ -0,0 +1,60 @@
require 'rails_helper'
describe Conversations::AssignmentService do
let(:account) { create(:account) }
let(:agent) { create(:user, account: account) }
let(:agent_bot) { create(:agent_bot, account: account) }
let(:conversation) { create(:conversation, account: account) }
describe '#perform' do
context 'when assignee_id is blank' do
before do
conversation.update!(assignee: agent, assignee_agent_bot: agent_bot)
end
it 'clears both human and bot assignees' do
described_class.new(conversation: conversation, assignee_id: nil).perform
conversation.reload
expect(conversation.assignee_id).to be_nil
expect(conversation.assignee_agent_bot_id).to be_nil
end
end
context 'when assigning a user' do
before do
conversation.update!(assignee_agent_bot: agent_bot, assignee: nil)
end
it 'sets the agent and clears agent bot' do
result = described_class.new(conversation: conversation, assignee_id: agent.id).perform
conversation.reload
expect(result).to eq(agent)
expect(conversation.assignee_id).to eq(agent.id)
expect(conversation.assignee_agent_bot_id).to be_nil
end
end
context 'when assigning an agent bot' do
let(:service) do
described_class.new(
conversation: conversation,
assignee_id: agent_bot.id,
assignee_type: 'AgentBot'
)
end
it 'sets the agent bot and clears human assignee' do
conversation.update!(assignee: agent, assignee_agent_bot: nil)
result = service.perform
conversation.reload
expect(result).to eq(agent_bot)
expect(conversation.assignee_agent_bot_id).to eq(agent_bot.id)
expect(conversation.assignee_id).to be_nil
end
end
end
end

View File

@@ -0,0 +1,254 @@
## This spec is to ensure alignment between frontend and backend filters
# ref: https://github.com/chatwoot/chatwoot/pull/11111
require 'rails_helper'
describe Conversations::FilterService do
describe 'Frontend alignment tests' do
let!(:account) { create(:account) }
let!(:user_1) { create(:user, account: account, role: :administrator) }
let!(:inbox) { create(:inbox, account: account) }
let!(:params) { { payload: [], page: 1 } }
before do
account.conversations.destroy_all
# Create inbox membership
create(:inbox_member, user: user_1, inbox: inbox)
# Create custom attribute definition for conversation_type
create(:custom_attribute_definition,
attribute_key: 'conversation_type',
account: account,
attribute_model: 'conversation_attribute',
attribute_display_type: 'list',
attribute_values: %w[platinum silver gold regular])
end
context 'with A AND B OR C filter chain' do
let(:conversation) { create(:conversation, account: account, inbox: inbox, assignee: user_1) }
let(:filter_payload) do
[
{
attribute_key: 'status',
filter_operator: 'equal_to',
values: ['open'],
query_operator: 'AND'
}.with_indifferent_access,
{
attribute_key: 'priority',
filter_operator: 'equal_to',
values: ['urgent'],
query_operator: 'OR'
}.with_indifferent_access,
{
attribute_key: 'display_id',
filter_operator: 'equal_to',
values: ['12345'],
query_operator: nil
}.with_indifferent_access
]
end
before do
conversation.update!(
status: 'open',
priority: 'urgent',
display_id: '12345',
additional_attributes: { 'browser_language': 'en' }
)
end
it 'matches when all conditions are true' do
params[:payload] = filter_payload
result = described_class.new(params, user_1, account).perform
expect(result[:conversations].length).to be 1
end
it 'matches when first condition is false but third is true' do
conversation.update!(status: 'resolved', priority: 'urgent', display_id: '12345')
params[:payload] = filter_payload
result = described_class.new(params, user_1, account).perform
expect(result[:conversations].length).to be 1
end
it 'matches when first and second condition is false but third is true' do
conversation.update!(status: 'resolved', priority: 'low', display_id: '12345')
params[:payload] = filter_payload
result = described_class.new(params, user_1, account).perform
expect(result[:conversations].length).to be 1
end
it 'does not match when all conditions are false' do
conversation.update!(status: 'resolved', priority: 'low', display_id: '67890')
params[:payload] = filter_payload
result = described_class.new(params, user_1, account).perform
expect(result[:conversations].length).to be 0
end
end
context 'with A OR B AND C filter chain' do
let(:conversation) { create(:conversation, account: account, inbox: inbox, assignee: user_1) }
let(:filter_payload) do
[
{
attribute_key: 'status',
filter_operator: 'equal_to',
values: ['open'],
query_operator: 'OR'
}.with_indifferent_access,
{
attribute_key: 'priority',
filter_operator: 'equal_to',
values: ['low'],
query_operator: 'AND'
}.with_indifferent_access,
{
attribute_key: 'display_id',
filter_operator: 'equal_to',
values: ['67890'],
query_operator: nil
}.with_indifferent_access
]
end
before do
conversation.update!(
status: 'open',
priority: 'urgent',
display_id: '12345',
additional_attributes: { 'browser_language': 'en' }
)
end
it 'matches when first condition is true' do
params[:payload] = filter_payload
result = described_class.new(params, user_1, account).perform
expect(result[:conversations].length).to be 1
end
it 'matches when second and third conditions are true' do
conversation.update!(status: 'resolved', priority: 'low', display_id: '67890')
params[:payload] = filter_payload
result = described_class.new(params, user_1, account).perform
expect(result[:conversations].length).to be 1
end
end
context 'with complex filter chain A AND B OR C AND D' do
let(:conversation) { create(:conversation, account: account, inbox: inbox, assignee: user_1) }
let(:filter_payload) do
[
{
attribute_key: 'status',
filter_operator: 'equal_to',
values: ['open'],
query_operator: 'AND'
}.with_indifferent_access,
{
attribute_key: 'priority',
filter_operator: 'equal_to',
values: ['urgent'],
query_operator: 'OR'
}.with_indifferent_access,
{
attribute_key: 'display_id',
filter_operator: 'equal_to',
values: ['67890'],
query_operator: 'AND'
}.with_indifferent_access,
{
attribute_key: 'browser_language',
filter_operator: 'equal_to',
values: ['tr'],
query_operator: nil
}.with_indifferent_access
]
end
before do
conversation.update!(
status: 'open',
priority: 'urgent',
display_id: '12345',
additional_attributes: { 'browser_language': 'en' },
custom_attributes: { conversation_type: 'platinum' }
)
end
it 'matches when first two conditions are true' do
params[:payload] = filter_payload
result = described_class.new(params, user_1, account).perform
expect(result[:conversations].length).to be 1
end
it 'matches when last two conditions are true' do
conversation.update!(
status: 'resolved',
priority: 'low',
display_id: '67890',
additional_attributes: { 'browser_language': 'tr' }
)
params[:payload] = filter_payload
result = described_class.new(params, user_1, account).perform
expect(result[:conversations].length).to be 1
end
end
context 'with mixed operators filter chain' do
let(:conversation) { create(:conversation, account: account, inbox: inbox, assignee: user_1) }
let(:filter_payload) do
[
{
attribute_key: 'status',
filter_operator: 'equal_to',
values: ['open'],
query_operator: 'AND'
}.with_indifferent_access,
{
attribute_key: 'priority',
filter_operator: 'equal_to',
values: ['urgent'],
query_operator: 'OR'
}.with_indifferent_access,
{
attribute_key: 'display_id',
filter_operator: 'equal_to',
values: ['67890'],
query_operator: 'AND'
}.with_indifferent_access,
{
attribute_key: 'conversation_type',
filter_operator: 'equal_to',
values: ['platinum'],
custom_attribute_type: '',
query_operator: nil
}.with_indifferent_access
]
end
before do
conversation.update!(
status: 'open',
priority: 'urgent',
display_id: '12345',
additional_attributes: { 'browser_language': 'en' },
custom_attributes: { conversation_type: 'platinum' }
)
end
it 'matches when all conditions in the chain are true' do
params[:payload] = filter_payload
result = described_class.new(params, user_1, account).perform
expect(result[:conversations].length).to be 1
end
it 'does not match when the last condition is false' do
conversation.update!(custom_attributes: { conversation_type: 'silver' })
params[:payload] = filter_payload
result = described_class.new(params, user_1, account).perform
expect(result[:conversations].length).to be 1
end
end
end
end

View File

@@ -0,0 +1,555 @@
require 'rails_helper'
describe Conversations::FilterService do
subject(:filter_service) { described_class }
let!(:account) { create(:account) }
let!(:user_1) { create(:user, account: account) }
let!(:user_2) { create(:user, account: account) }
let!(:campaign_1) { create(:campaign, title: 'Test Campaign', account: account) }
let!(:campaign_2) { create(:campaign, title: 'Campaign', account: account) }
let!(:inbox) { create(:inbox, account: account, enable_auto_assignment: false) }
let!(:user_2_assigned_conversation) { create(:conversation, account: account, inbox: inbox, assignee: user_2) }
let!(:en_conversation_1) do
create(:conversation, account: account, inbox: inbox, assignee: user_1, campaign_id: campaign_1.id,
status: 'pending', additional_attributes: { 'browser_language': 'en' })
end
let!(:en_conversation_2) do
create(:conversation, account: account, inbox: inbox, assignee: user_1, campaign_id: campaign_2.id,
status: 'pending', additional_attributes: { 'browser_language': 'en' })
end
before do
create(:inbox_member, user: user_1, inbox: inbox)
create(:inbox_member, user: user_2, inbox: inbox)
en_conversation_1.update!(custom_attributes: { conversation_additional_information: 'test custom data' })
en_conversation_2.update!(custom_attributes: { conversation_additional_information: 'test custom data', conversation_type: 'platinum' })
user_2_assigned_conversation.update!(custom_attributes: { conversation_type: 'platinum', conversation_created: '2022-01-19' })
create(:conversation, account: account, inbox: inbox, assignee: user_1)
create(:custom_attribute_definition,
attribute_key: 'conversation_type',
account: account,
attribute_model: 'conversation_attribute',
attribute_display_type: 'list',
attribute_values: %w[regular platinum gold])
create(:custom_attribute_definition,
attribute_key: 'conversation_created',
account: account,
attribute_model: 'conversation_attribute',
attribute_display_type: 'date')
create(:custom_attribute_definition,
attribute_key: 'conversation_additional_information',
account: account,
attribute_model: 'conversation_attribute',
attribute_display_type: 'text')
end
describe '#perform' do
context 'with query present' do
let!(:params) { { payload: [], page: 1 } }
let(:payload) do
[
{
attribute_key: 'browser_language',
filter_operator: 'equal_to',
values: 'en',
query_operator: 'AND',
custom_attribute_type: ''
}.with_indifferent_access,
{
attribute_key: 'status',
filter_operator: 'not_equal_to',
values: %w[resolved],
query_operator: nil,
custom_attribute_type: ''
}.with_indifferent_access
]
end
it 'filter conversations by additional_attributes and status' do
params[:payload] = payload
result = filter_service.new(params, user_1, account).perform
conversations = Conversation.where("additional_attributes ->> 'browser_language' IN (?) AND status IN (?)", ['en'], [1, 2])
expect(result[:count][:all_count]).to be conversations.count
end
it 'filter conversations by priority' do
conversation = create(:conversation, account: account, inbox: inbox, assignee: user_1, priority: :high)
params[:payload] = [
{
attribute_key: 'priority',
filter_operator: 'equal_to',
values: ['high'],
query_operator: nil,
custom_attribute_type: ''
}.with_indifferent_access
]
result = filter_service.new(params, user_1, account).perform
expect(result[:conversations].length).to eq 1
expect(result[:conversations][0][:id]).to eq conversation.id
end
it 'filter conversations by multiple priority values' do
high_priority = create(:conversation, account: account, inbox: inbox, assignee: user_1, priority: :high)
urgent_priority = create(:conversation, account: account, inbox: inbox, assignee: user_1, priority: :urgent)
create(:conversation, account: account, inbox: inbox, assignee: user_1, priority: :low)
params[:payload] = [
{
attribute_key: 'priority',
filter_operator: 'equal_to',
values: %w[high urgent],
query_operator: nil,
custom_attribute_type: ''
}.with_indifferent_access
]
result = filter_service.new(params, user_1, account).perform
expect(result[:conversations].length).to eq 2
expect(result[:conversations].pluck(:id)).to include(high_priority.id, urgent_priority.id)
end
it 'filter conversations with not_equal_to priority operator' do
create(:conversation, account: account, inbox: inbox, assignee: user_1, priority: :high)
create(:conversation, account: account, inbox: inbox, assignee: user_1, priority: :urgent)
low_priority = create(:conversation, account: account, inbox: inbox, assignee: user_1, priority: :low)
medium_priority = create(:conversation, account: account, inbox: inbox, assignee: user_1, priority: :medium)
params[:payload] = [
{
attribute_key: 'priority',
filter_operator: 'not_equal_to',
values: %w[high urgent],
query_operator: nil,
custom_attribute_type: ''
}.with_indifferent_access
]
result = filter_service.new(params, user_1, account).perform
# Only include conversations with medium and low priority, excluding high and urgent
expect(result[:conversations].length).to eq 2
expect(result[:conversations].pluck(:id)).to include(low_priority.id, medium_priority.id)
end
it 'filter conversations by additional_attributes and status with pagination' do
params[:payload] = payload
params[:page] = 2
result = filter_service.new(params, user_1, account).perform
conversations = Conversation.where("additional_attributes ->> 'browser_language' IN (?) AND status IN (?)", ['en'], [1, 2])
expect(result[:count][:all_count]).to be conversations.count
end
it 'filters items with contains filter_operator with values being an array' do
params[:payload] = [{
attribute_key: 'browser_language',
filter_operator: 'equal_to',
values: %w[tr fr],
query_operator: '',
custom_attribute_type: ''
}.with_indifferent_access]
create(:conversation, account: account, inbox: inbox, assignee: user_1, campaign_id: campaign_1.id,
status: 'pending', additional_attributes: { 'browser_language': 'fr' })
create(:conversation, account: account, inbox: inbox, assignee: user_1, campaign_id: campaign_1.id,
status: 'pending', additional_attributes: { 'browser_language': 'tr' })
result = filter_service.new(params, user_1, account).perform
expect(result[:count][:all_count]).to be 2
end
it 'filters items with does not contain filter operator with values being an array' do
params[:payload] = [{
attribute_key: 'browser_language',
filter_operator: 'not_equal_to',
values: %w[tr en],
query_operator: '',
custom_attribute_type: ''
}.with_indifferent_access]
create(:conversation, account: account, inbox: inbox, assignee: user_1, campaign_id: campaign_1.id,
status: 'pending', additional_attributes: { 'browser_language': 'fr' })
create(:conversation, account: account, inbox: inbox, assignee: user_1, campaign_id: campaign_1.id,
status: 'pending', additional_attributes: { 'browser_language': 'tr' })
result = filter_service.new(params, user_1, account).perform
expect(result[:count][:all_count]).to be 1
expect(result[:conversations].first.additional_attributes['browser_language']).to eq 'fr'
end
it 'filter conversations by additional_attributes with NOT_IN filter' do
payload = [{ attribute_key: 'conversation_type', filter_operator: 'not_equal_to', values: 'platinum', query_operator: nil,
custom_attribute_type: 'conversation_attribute' }.with_indifferent_access]
params[:payload] = payload
result = filter_service.new(params, user_1, account).perform
conversations = Conversation.where(
"custom_attributes ->> 'conversation_type' NOT IN (?) OR custom_attributes ->> 'conversation_type' IS NULL", ['platinum']
)
expect(result[:count][:all_count]).to be conversations.count
end
it 'filter conversations by tags' do
user_2_assigned_conversation.update_labels('support')
params[:payload] = [
{
attribute_key: 'assignee_id',
filter_operator: 'equal_to',
values: [user_1.id, user_2.id],
query_operator: 'AND'
}.with_indifferent_access,
{
attribute_key: 'labels',
filter_operator: 'equal_to',
values: ['support'],
query_operator: 'AND'
}.with_indifferent_access,
{
attribute_key: 'labels',
filter_operator: 'not_equal_to',
values: ['random-label'],
query_operator: nil
}.with_indifferent_access
]
result = filter_service.new(params, user_1, account).perform
expect(result[:count][:all_count]).to be 1
end
it 'filter conversations by is_present filter_operator' do
params[:payload] = [
{
attribute_key: 'assignee_id',
filter_operator: 'equal_to',
values: [
user_1.id,
user_2.id
],
query_operator: 'AND',
custom_attribute_type: ''
}.with_indifferent_access,
{
attribute_key: 'campaign_id',
filter_operator: 'is_present',
values: [],
query_operator: nil,
custom_attribute_type: ''
}.with_indifferent_access
]
result = filter_service.new(params, user_1, account).perform
expect(result[:count][:all_count]).to be 2
expect(result[:conversations].pluck(:campaign_id).sort).to eq [campaign_2.id, campaign_1.id].sort
end
it 'handles invalid query conditions' do
params[:payload] = [
{
attribute_key: 'assignee_id',
filter_operator: 'equal_to',
values: [
user_1.id,
user_2.id
],
query_operator: 'INVALID',
custom_attribute_type: ''
}.with_indifferent_access,
{
attribute_key: 'campaign_id',
filter_operator: 'is_present',
values: [],
query_operator: nil,
custom_attribute_type: ''
}.with_indifferent_access
]
expect { filter_service.new(params, user_1, account).perform }.to raise_error(CustomExceptions::CustomFilter::InvalidQueryOperator)
end
end
end
describe '#perform on custom attribute' do
context 'with query present' do
let!(:params) { { payload: [], page: 1 } }
it 'filter by custom_attributes and labels' do
user_2_assigned_conversation.update_labels('support')
params[:payload] = [
{
attribute_key: 'conversation_type',
filter_operator: 'equal_to',
values: ['platinum'],
query_operator: 'AND'
}.with_indifferent_access,
{
attribute_key: 'conversation_created',
filter_operator: 'is_less_than',
values: ['2022-01-20'],
query_operator: 'OR',
custom_attribute_type: ''
}.with_indifferent_access,
{
attribute_key: 'labels',
filter_operator: 'equal_to',
values: ['support'],
query_operator: nil
}.with_indifferent_access
]
result = filter_service.new(params, user_1, account).perform
expect(result[:conversations].length).to be 1
expect(result[:conversations][0][:id]).to be user_2_assigned_conversation.id
end
it 'filter by custom_attributes and labels with custom_attribute_type nil' do
user_2_assigned_conversation.update_labels('support')
params[:payload] = [
{
attribute_key: 'conversation_type',
filter_operator: 'equal_to',
values: ['platinum'],
query_operator: 'AND'
}.with_indifferent_access,
{
attribute_key: 'conversation_created',
filter_operator: 'is_less_than',
values: ['2022-01-20'],
query_operator: 'OR',
custom_attribute_type: nil
}.with_indifferent_access,
{
attribute_key: 'labels',
filter_operator: 'equal_to',
values: ['support'],
query_operator: nil
}.with_indifferent_access
]
result = filter_service.new(params, user_1, account).perform
expect(result[:conversations].length).to be 1
expect(result[:conversations][0][:id]).to be user_2_assigned_conversation.id
end
it 'filter by custom_attributes' do
params[:payload] = [
{
attribute_key: 'conversation_type',
filter_operator: 'equal_to',
values: ['platinum'],
query_operator: 'AND',
custom_attribute_type: ''
}.with_indifferent_access,
{
attribute_key: 'conversation_created',
filter_operator: 'is_less_than',
values: ['2022-01-20'],
query_operator: nil,
custom_attribute_type: ''
}.with_indifferent_access
]
result = filter_service.new(params, user_1, account).perform
expect(result[:conversations].length).to be 1
end
it 'filter by custom_attributes with custom_attribute_type nil' do
params[:payload] = [
{
attribute_key: 'conversation_type',
filter_operator: 'equal_to',
values: ['platinum'],
query_operator: 'AND',
custom_attribute_type: nil
}.with_indifferent_access,
{
attribute_key: 'conversation_created',
filter_operator: 'is_less_than',
values: ['2022-01-20'],
query_operator: nil,
custom_attribute_type: nil
}.with_indifferent_access
]
result = filter_service.new(params, user_1, account).perform
expect(result[:conversations].length).to be 1
end
it 'filter by custom_attributes and additional_attributes' do
conversations = user_1.conversations
conversations[0].update!(additional_attributes: { 'browser_language': 'en' }, custom_attributes: { conversation_type: 'silver' })
conversations[1].update!(additional_attributes: { 'browser_language': 'en' }, custom_attributes: { conversation_type: 'platinum' })
conversations[2].update!(additional_attributes: { 'browser_language': 'tr' }, custom_attributes: { conversation_type: 'platinum' })
params[:payload] = [
{
attribute_key: 'conversation_type',
filter_operator: 'equal_to',
values: ['platinum'],
query_operator: 'AND',
custom_attribute_type: ''
}.with_indifferent_access,
{
attribute_key: 'browser_language',
filter_operator: 'not_equal_to',
values: 'en',
query_operator: nil,
custom_attribute_type: ''
}.with_indifferent_access
]
result = filter_service.new(params, user_1, account).perform
expect(result[:conversations].length).to be 1
end
end
end
describe '#perform on date filter' do
context 'with query present' do
let!(:params) { { payload: [], page: 1 } }
it 'filter by created_at' do
params[:payload] = [
{
attribute_key: 'created_at',
filter_operator: 'is_greater_than',
values: ['2022-01-20'],
query_operator: nil,
custom_attribute_type: ''
}.with_indifferent_access
]
result = filter_service.new(params, user_1, account).perform
expected_count = Conversation.where('created_at > ?', DateTime.parse('2022-01-20')).count
expect(result[:conversations].length).to be expected_count
end
it 'filter by created_at and conversation_type' do
params[:payload] = [
{
attribute_key: 'conversation_type',
filter_operator: 'equal_to',
values: ['platinum'],
query_operator: 'AND',
custom_attribute_type: ''
}.with_indifferent_access,
{
attribute_key: 'created_at',
filter_operator: 'is_greater_than',
values: ['2022-01-20'],
query_operator: nil,
custom_attribute_type: ''
}.with_indifferent_access
]
result = filter_service.new(params, user_1, account).perform
expected_count = Conversation.where("created_at > ? AND custom_attributes->>'conversation_type' = ?", DateTime.parse('2022-01-20'),
'platinum').count
expect(result[:conversations].length).to be expected_count
end
context 'with x_days_before filter' do
before do
Time.zone = 'UTC'
en_conversation_1.update!(last_activity_at: (Time.zone.today - 4.days))
en_conversation_2.update!(last_activity_at: (Time.zone.today - 5.days))
user_2_assigned_conversation.update!(last_activity_at: (Time.zone.today - 2.days))
end
it 'filter by last_activity_at 3_days_before and custom_attributes' do
params[:payload] = [
{
attribute_key: 'last_activity_at',
filter_operator: 'days_before',
values: [3],
query_operator: 'AND',
custom_attribute_type: ''
}.with_indifferent_access,
{
attribute_key: 'conversation_type',
filter_operator: 'equal_to',
values: ['platinum'],
query_operator: nil,
custom_attribute_type: ''
}.with_indifferent_access
]
expected_count = Conversation.where("last_activity_at < ? AND custom_attributes->>'conversation_type' = ?", (Time.zone.today - 3.days),
'platinum').count
result = filter_service.new(params, user_1, account).perform
expect(result[:conversations].length).to be expected_count
end
it 'filter by last_activity_at 2_days_before' do
params[:payload] = [
{
attribute_key: 'last_activity_at',
filter_operator: 'days_before',
values: [3],
query_operator: nil,
custom_attribute_type: ''
}.with_indifferent_access
]
expected_count = Conversation.where('last_activity_at < ?', (Time.zone.today - 2.days)).count
result = filter_service.new(params, user_1, account).perform
expect(result[:conversations].length).to be expected_count
end
end
end
end
describe '#perform on date filter with no current account' do
before do
Current.account = nil
end
context 'with query present' do
let!(:params) { { payload: [], page: 1 } }
it 'filter by created_at' do
params[:payload] = [
{
attribute_key: 'created_at',
filter_operator: 'is_greater_than',
values: ['2022-01-20'],
query_operator: nil,
custom_attribute_type: ''
}.with_indifferent_access
]
result = filter_service.new(params, user_1, account).perform
expected_count = Conversation.where('created_at > ?', DateTime.parse('2022-01-20')).count
expect(Current.account).to be_nil
expect(result[:conversations].length).to be expected_count
end
end
end
describe '#base_relation' do
let!(:account) { create(:account) }
let!(:user_1) { create(:user, account: account, role: :agent) }
let!(:admin) { create(:user, account: account, role: :administrator) }
let!(:inbox_1) { create(:inbox, account: account) }
let!(:inbox_2) { create(:inbox, account: account) }
let!(:params) { { payload: [], page: 1 } }
before do
account.conversations.destroy_all
# Make user_1 a regular agent with access to inbox_1 only
create(:inbox_member, user: user_1, inbox: inbox_1)
# Create conversations in both inboxes
create(:conversation, account: account, inbox: inbox_1)
create(:conversation, account: account, inbox: inbox_2)
end
it 'returns all conversations for administrators, even for inboxes they are not members of' do
service = filter_service.new(params, admin, account)
result = service.perform
expect(result[:conversations].count).to eq 2
end
it 'filters conversations by inbox membership for non-administrators' do
service = filter_service.new(params, user_1, account)
result = service.perform
expect(result[:conversations].count).to eq 1
end
end
end

View File

@@ -0,0 +1,628 @@
require 'rails_helper'
RSpec.describe Conversations::MessageWindowService do
describe 'on API channels' do
let!(:api_channel) { create(:channel_api, additional_attributes: {}) }
let!(:api_channel_with_limit) { create(:channel_api, additional_attributes: { agent_reply_time_window: '12' }) }
context 'when agent_reply_time_window is not configured' do
it 'return true irrespective of the last message time' do
conversation = create(:conversation, inbox: api_channel.inbox)
create(
:message,
account: conversation.account,
inbox: api_channel.inbox,
conversation: conversation,
created_at: 13.hours.ago
)
service = described_class.new(conversation)
expect(api_channel.additional_attributes['agent_reply_time_window']).to be_nil
expect(service.can_reply?).to be true
end
end
context 'when agent_reply_time_window is configured' do
it 'return false if it is outside of agent_reply_time_window' do
conversation = create(:conversation, inbox: api_channel_with_limit.inbox)
create(
:message,
account: conversation.account,
inbox: api_channel_with_limit.inbox,
conversation: conversation,
created_at: 13.hours.ago
)
service = described_class.new(conversation)
expect(api_channel_with_limit.additional_attributes['agent_reply_time_window']).to eq '12'
expect(service.can_reply?).to be false
end
it 'return true if it is inside of agent_reply_time_window' do
conversation = create(:conversation, inbox: api_channel_with_limit.inbox)
create(
:message,
account: conversation.account,
inbox: api_channel_with_limit.inbox,
conversation: conversation
)
service = described_class.new(conversation)
expect(service.can_reply?).to be true
end
end
end
describe 'on Facebook channels' do
before do
stub_request(:post, /graph.facebook.com/)
GlobalConfig.clear_cache
end
let!(:facebook_channel) { create(:channel_facebook_page) }
let!(:facebook_inbox) { create(:inbox, channel: facebook_channel, account: facebook_channel.account) }
let!(:conversation) { create(:conversation, inbox: facebook_inbox, account: facebook_channel.account) }
context 'when the HUMAN_AGENT is enabled' do
it 'return false if the last message is outgoing' do
with_modified_env ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT: 'true' do
service = described_class.new(conversation)
expect(service.can_reply?).to be false
end
end
it 'return true if the last message is incoming and within the messaging window (with in 24 hours)' do
with_modified_env ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT: 'true' do
create(
:message,
account: conversation.account,
inbox: facebook_inbox,
conversation: conversation,
created_at: 13.hours.ago
)
service = described_class.new(conversation)
expect(service.can_reply?).to be true
end
end
it 'return true if the last message is incoming and within the messaging window (with in 7 days)' do
with_modified_env ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT: 'true' do
create(
:message,
account: conversation.account,
inbox: facebook_inbox,
conversation: conversation,
created_at: 5.days.ago
)
service = described_class.new(conversation)
expect(service.can_reply?).to be true
end
end
it 'return false if the last message is incoming and outside the messaging window (8 days ago )' do
with_modified_env ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT: 'true' do
create(
:message,
account: conversation.account,
inbox: facebook_inbox,
conversation: conversation,
created_at: 8.days.ago
)
service = described_class.new(conversation)
expect(service.can_reply?).to be false
end
end
it 'return true if last message is outgoing but previous incoming message is within window' do
with_modified_env ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT: 'true' do
create(
:message,
account: conversation.account,
inbox: facebook_inbox,
conversation: conversation,
message_type: :incoming,
created_at: 6.hours.ago
)
create(
:message,
account: conversation.account,
inbox: facebook_inbox,
conversation: conversation,
message_type: :outgoing,
created_at: 1.hour.ago
)
service = described_class.new(conversation)
expect(service.can_reply?).to be true
end
end
it 'considers only the last incoming message for determining time window' do
with_modified_env ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT: 'true' do
# Old message outside window
create(
:message,
account: conversation.account,
inbox: facebook_inbox,
conversation: conversation,
created_at: 10.days.ago
)
# Recent message within window
create(
:message,
account: conversation.account,
inbox: facebook_inbox,
conversation: conversation,
created_at: 6.hours.ago
)
service = described_class.new(conversation)
expect(service.can_reply?).to be true
end
end
end
context 'when the HUMAN_AGENT is disabled' do
with_modified_env ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT: 'false' do
it 'return false if the last message is outgoing' do
service = described_class.new(conversation)
expect(service.can_reply?).to be false
end
it 'return false if the last message is incoming and outside the messaging window ( 8 days ago )' do
create(
:message,
account: conversation.account,
inbox: facebook_inbox,
conversation: conversation,
created_at: 4.days.ago
)
service = described_class.new(conversation)
expect(service.can_reply?).to be false
end
it 'return true if the last message is incoming and within the messaging window (24 hours limit)' do
create(
:message,
account: conversation.account,
inbox: facebook_inbox,
conversation: conversation,
created_at: 13.hours.ago
)
service = described_class.new(conversation)
expect(service.can_reply?).to be true
end
end
end
end
describe 'on Instagram channels' do
let!(:instagram_channel) { create(:channel_instagram) }
let!(:instagram_inbox) { create(:inbox, channel: instagram_channel, account: instagram_channel.account) }
let!(:conversation) { create(:conversation, inbox: instagram_inbox, account: instagram_channel.account) }
context 'when the HUMAN_AGENT is enabled' do
it 'return false if the last message is outgoing' do
with_modified_env ENABLE_INSTAGRAM_CHANNEL_HUMAN_AGENT: 'true' do
service = described_class.new(conversation)
expect(service.can_reply?).to be false
end
end
it 'return true if the last message is incoming and within the messaging window (with in 24 hours)' do
with_modified_env ENABLE_INSTAGRAM_CHANNEL_HUMAN_AGENT: 'true' do
create(
:message,
account: conversation.account,
inbox: instagram_inbox,
conversation: conversation,
created_at: 13.hours.ago
)
service = described_class.new(conversation)
expect(service.can_reply?).to be true
end
end
it 'return true if the last message is incoming and within the messaging window (with in 7 days)' do
with_modified_env ENABLE_INSTAGRAM_CHANNEL_HUMAN_AGENT: 'true' do
create(
:message,
account: conversation.account,
inbox: instagram_inbox,
conversation: conversation,
created_at: 6.days.ago
)
service = described_class.new(conversation)
expect(service.can_reply?).to be true
end
end
it 'return false if the last message is incoming and outside the messaging window (8 days ago)' do
with_modified_env ENABLE_INSTAGRAM_CHANNEL_HUMAN_AGENT: 'true' do
create(
:message,
account: conversation.account,
inbox: instagram_inbox,
conversation: conversation,
created_at: 8.days.ago
)
service = described_class.new(conversation)
expect(service.can_reply?).to be false
end
end
it 'return true if last message is outgoing but previous incoming message is within window' do
with_modified_env ENABLE_INSTAGRAM_CHANNEL_HUMAN_AGENT: 'true' do
create(
:message,
account: conversation.account,
inbox: instagram_inbox,
conversation: conversation,
message_type: :incoming,
created_at: 6.hours.ago
)
create(
:message,
account: conversation.account,
inbox: instagram_inbox,
conversation: conversation,
message_type: :outgoing,
created_at: 1.hour.ago
)
service = described_class.new(conversation)
expect(service.can_reply?).to be true
end
end
it 'considers only the last incoming message for determining time window' do
with_modified_env ENABLE_INSTAGRAM_CHANNEL_HUMAN_AGENT: 'true' do
# Old message outside window
create(
:message,
account: conversation.account,
inbox: instagram_inbox,
conversation: conversation,
created_at: 10.days.ago
)
# Recent message within window
create(
:message,
account: conversation.account,
inbox: instagram_inbox,
conversation: conversation,
created_at: 6.hours.ago
)
service = described_class.new(conversation)
expect(service.can_reply?).to be true
end
end
it 'return true if the last message is incoming and exactly at the edge of 24-hour window with HUMAN_AGENT disabled' do
with_modified_env ENABLE_INSTAGRAM_CHANNEL_HUMAN_AGENT: 'true' do
create(
:message,
account: conversation.account,
inbox: instagram_inbox,
conversation: conversation,
created_at: 24.hours.ago
)
service = described_class.new(conversation)
expect(service.can_reply?).to be true
end
end
end
context 'when the HUMAN_AGENT is disabled' do
it 'return false if the last message is outgoing' do
with_modified_env ENABLE_INSTAGRAM_CHANNEL_HUMAN_AGENT: 'false' do
service = described_class.new(conversation)
expect(service.can_reply?).to be false
end
end
it 'return false if the last message is incoming and outside the messaging window (8 days ago)' do
with_modified_env ENABLE_INSTAGRAM_CHANNEL_HUMAN_AGENT: 'false' do
create(
:message,
account: conversation.account,
inbox: instagram_inbox,
conversation: conversation,
created_at: 9.days.ago
)
service = described_class.new(conversation)
expect(service.can_reply?).to be false
end
end
it 'return true if the last message is incoming and within the messaging window (24 hours limit)' do
with_modified_env ENABLE_INSTAGRAM_CHANNEL_HUMAN_AGENT: 'false' do
create(
:message,
account: conversation.account,
inbox: instagram_inbox,
conversation: conversation,
created_at: 13.hours.ago
)
service = described_class.new(conversation)
expect(service.can_reply?).to be true
end
end
end
end
describe 'on WhatsApp Cloud channels' do
let!(:whatsapp_channel) { create(:channel_whatsapp, provider: 'whatsapp_cloud', sync_templates: false, validate_provider_config: false) }
let!(:whatsapp_inbox) { create(:inbox, channel: whatsapp_channel, account: whatsapp_channel.account) }
let!(:conversation) { create(:conversation, inbox: whatsapp_inbox, account: whatsapp_channel.account) }
it 'return false if the last message is outgoing' do
service = described_class.new(conversation)
expect(service.can_reply?).to be false
end
it 'return true if the last message is incoming and within the messaging window (with in 24 hours)' do
create(
:message,
account: conversation.account,
inbox: whatsapp_inbox,
conversation: conversation,
created_at: 13.hours.ago
)
service = described_class.new(conversation)
expect(service.can_reply?).to be true
end
it 'return false if the last message is incoming and outside the messaging window (24 hours limit)' do
create(
:message,
account: conversation.account,
inbox: whatsapp_inbox,
conversation: conversation,
created_at: 25.hours.ago
)
service = described_class.new(conversation)
expect(service.can_reply?).to be false
end
it 'return true if last message is outgoing but previous incoming message is within window' do
create(
:message,
account: conversation.account,
inbox: whatsapp_inbox,
conversation: conversation,
message_type: :incoming,
created_at: 6.hours.ago
)
create(
:message,
account: conversation.account,
inbox: whatsapp_inbox,
conversation: conversation,
message_type: :outgoing,
created_at: 1.hour.ago
)
service = described_class.new(conversation)
expect(service.can_reply?).to be true
end
it 'considers only the last incoming message for determining time window' do
# Old message outside window
create(
:message,
account: conversation.account,
inbox: whatsapp_inbox,
conversation: conversation,
created_at: 10.days.ago
)
# Recent message within window
create(
:message,
account: conversation.account,
inbox: whatsapp_inbox,
conversation: conversation,
created_at: 6.hours.ago
)
service = described_class.new(conversation)
expect(service.can_reply?).to be true
end
end
describe 'on Web widget channels' do
let!(:widget_channel) { create(:channel_widget) }
let!(:widget_inbox) { create(:inbox, channel: widget_channel, account: widget_channel.account) }
let!(:conversation) { create(:conversation, inbox: widget_inbox, account: widget_channel.account) }
it 'return true irrespective of the last message time' do
create(
:message,
account: conversation.account,
inbox: widget_inbox,
conversation: conversation,
created_at: 13.hours.ago
)
service = described_class.new(conversation)
expect(service.can_reply?).to be true
end
end
describe 'on SMS channels' do
let!(:sms_channel) { create(:channel_sms) }
let!(:sms_inbox) { create(:inbox, channel: sms_channel, account: sms_channel.account) }
let!(:conversation) { create(:conversation, inbox: sms_inbox, account: sms_channel.account) }
it 'return true irrespective of the last message time' do
create(
:message,
account: conversation.account,
inbox: sms_inbox,
conversation: conversation,
created_at: 13.hours.ago
)
service = described_class.new(conversation)
expect(service.can_reply?).to be true
end
end
describe 'on Telegram channels' do
let!(:telegram_channel) { create(:channel_telegram) }
let!(:telegram_inbox) { create(:inbox, channel: telegram_channel, account: telegram_channel.account) }
let!(:conversation) { create(:conversation, inbox: telegram_inbox, account: telegram_channel.account) }
it 'return true irrespective of the last message time' do
create(
:message,
account: conversation.account,
inbox: telegram_inbox,
conversation: conversation,
created_at: 13.hours.ago
)
service = described_class.new(conversation)
expect(service.can_reply?).to be true
end
end
describe 'on Email channels' do
let!(:email_channel) { create(:channel_email) }
let!(:email_inbox) { create(:inbox, channel: email_channel, account: email_channel.account) }
let!(:conversation) { create(:conversation, inbox: email_inbox, account: email_channel.account) }
it 'return true irrespective of the last message time' do
create(
:message,
account: conversation.account,
inbox: email_inbox,
conversation: conversation,
created_at: 13.hours.ago
)
service = described_class.new(conversation)
expect(service.can_reply?).to be true
end
end
describe 'on Line channels' do
let!(:line_channel) { create(:channel_line) }
let!(:line_inbox) { create(:inbox, channel: line_channel, account: line_channel.account) }
let!(:conversation) { create(:conversation, inbox: line_inbox, account: line_channel.account) }
it 'return true irrespective of the last message time' do
create(
:message,
account: conversation.account,
inbox: line_inbox,
conversation: conversation,
created_at: 13.hours.ago
)
service = described_class.new(conversation)
expect(service.can_reply?).to be true
end
end
describe 'on Twilio SMS channels' do
let!(:twilio_sms_channel) { create(:channel_twilio_sms) }
let!(:twilio_sms_inbox) { create(:inbox, channel: twilio_sms_channel, account: twilio_sms_channel.account) }
let!(:conversation) { create(:conversation, inbox: twilio_sms_inbox, account: twilio_sms_channel.account) }
it 'return true irrespective of the last message time' do
create(
:message,
account: conversation.account,
inbox: twilio_sms_inbox,
conversation: conversation,
created_at: 13.hours.ago
)
service = described_class.new(conversation)
expect(service.can_reply?).to be true
end
end
describe 'on WhatsApp Twilio channels' do
let!(:whatsapp_channel) { create(:channel_twilio_sms, medium: :whatsapp) }
let!(:whatsapp_inbox) { create(:inbox, channel: whatsapp_channel, account: whatsapp_channel.account) }
let!(:conversation) { create(:conversation, inbox: whatsapp_inbox, account: whatsapp_channel.account) }
it 'return false if the last message is outgoing' do
service = described_class.new(conversation)
expect(service.can_reply?).to be false
end
it 'return true if the last message is incoming and within the messaging window (with in 24 hours)' do
create(
:message,
account: conversation.account,
inbox: whatsapp_inbox,
conversation: conversation,
created_at: 13.hours.ago
)
service = described_class.new(conversation)
expect(service.can_reply?).to be true
end
it 'return false if the last message is incoming and outside the messaging window (24 hours limit)' do
create(
:message,
account: conversation.account,
inbox: whatsapp_inbox,
conversation: conversation,
created_at: 25.hours.ago
)
service = described_class.new(conversation)
expect(service.can_reply?).to be false
end
it 'return true if last message is outgoing but previous incoming message is within window' do
create(
:message,
account: conversation.account,
inbox: whatsapp_inbox,
conversation: conversation,
message_type: :incoming,
created_at: 6.hours.ago
)
create(
:message,
account: conversation.account,
inbox: whatsapp_inbox,
conversation: conversation,
message_type: :outgoing,
created_at: 1.hour.ago
)
service = described_class.new(conversation)
expect(service.can_reply?).to be true
end
it 'considers only the last incoming message for determining time window' do
# Old message outside window
create(
:message,
account: conversation.account,
inbox: whatsapp_inbox,
conversation: conversation,
created_at: 10.days.ago
)
# Recent message within window
create(
:message,
account: conversation.account,
inbox: whatsapp_inbox,
conversation: conversation,
created_at: 6.hours.ago
)
service = described_class.new(conversation)
expect(service.can_reply?).to be true
end
end
end

View File

@@ -0,0 +1,47 @@
require 'rails_helper'
RSpec.describe Conversations::PermissionFilterService do
let(:account) { create(:account) }
let!(:conversation) { create(:conversation, account: account, inbox: inbox) }
let!(:another_conversation) { create(:conversation, account: account, inbox: inbox) }
let(:admin) { create(:user, account: account, role: :administrator) }
let(:agent) { create(:user, account: account, role: :agent) }
let!(:inbox) { create(:inbox, account: account) }
# This inbox_member is used to establish the agent's access to the inbox
before { create(:inbox_member, user: agent, inbox: inbox) }
describe '#perform' do
context 'when user is an administrator' do
it 'returns all conversations' do
result = described_class.new(
account.conversations,
admin,
account
).perform
expect(result).to include(conversation)
expect(result).to include(another_conversation)
expect(result.count).to eq(2)
end
end
context 'when user is an agent' do
it 'returns all conversations with no further filtering' do
inbox_ids = agent.inboxes.where(account_id: account.id).pluck(:id)
# The base implementation returns all conversations
# expecting the caller to filter by assigned inboxes
result = described_class.new(
account.conversations.where(inbox_id: inbox_ids),
agent,
account
).perform
expect(result).to include(conversation)
expect(result).to include(another_conversation)
expect(result.count).to eq(2)
end
end
end
end