chore: Add controllers for conversation participants (#6462)

Co-authored-by: Aswin Dev P.S <aswindevps@gmail.com>
Co-authored-by: Sojan Jose <sojan@chatwoot.com>
This commit is contained in:
Pranav Raj S
2023-02-15 16:33:31 -08:00
committed by GitHub
parent 949ddf68ba
commit 7044eda281
34 changed files with 546 additions and 63 deletions

View File

@@ -17,7 +17,6 @@ Metrics/ClassLength:
- 'app/builders/messages/facebook/message_builder.rb' - 'app/builders/messages/facebook/message_builder.rb'
- 'app/controllers/api/v1/accounts/contacts_controller.rb' - 'app/controllers/api/v1/accounts/contacts_controller.rb'
- 'app/listeners/action_cable_listener.rb' - 'app/listeners/action_cable_listener.rb'
- 'app/models/conversation.rb'
RSpec/ExampleLength: RSpec/ExampleLength:
Max: 25 Max: 25
Style/Documentation: Style/Documentation:
@@ -188,4 +187,3 @@ AllCops:
- db/migrate/20200927135222_add_last_activity_at_to_conversation.rb - db/migrate/20200927135222_add_last_activity_at_to_conversation.rb
- db/migrate/20210306170117_add_last_activity_at_to_contacts.rb - db/migrate/20210306170117_add_last_activity_at_to_contacts.rb
- db/migrate/20220809104508_revert_cascading_indexes.rb - db/migrate/20220809104508_revert_cascading_indexes.rb

View File

@@ -0,0 +1,41 @@
class Api::V1::Accounts::Conversations::ParticipantsController < Api::V1::Accounts::Conversations::BaseController
def show
@participants = @conversation.conversation_participants
end
def create
ActiveRecord::Base.transaction do
@participants = participants_to_be_added_ids.map { |user_id| @conversation.conversation_participants.find_or_create_by(user_id: user_id) }
end
end
def update
ActiveRecord::Base.transaction do
participants_to_be_added_ids.each { |user_id| @conversation.conversation_participants.find_or_create_by(user_id: user_id) }
participants_to_be_removed_ids.each { |user_id| @conversation.conversation_participants.find_by(user_id: user_id)&.destroy }
end
@participants = @conversation.conversation_participants
render action: 'show'
end
def destroy
ActiveRecord::Base.transaction do
params[:user_ids].map { |user_id| @conversation.conversation_participants.find_by(user_id: user_id)&.destroy }
end
head :ok
end
private
def participants_to_be_added_ids
params[:user_ids] - current_participant_ids
end
def participants_to_be_removed_ids
current_participant_ids - params[:user_ids]
end
def current_participant_ids
@current_participant_ids ||= @conversation.conversation_participants.pluck(:user_id)
end
end

View File

@@ -97,6 +97,8 @@ class ConversationFinder
when 'mention' when 'mention'
conversation_ids = current_account.mentions.where(user: current_user).pluck(:conversation_id) conversation_ids = current_account.mentions.where(user: current_user).pluck(:conversation_id)
@conversations = @conversations.where(id: conversation_ids) @conversations = @conversations.where(id: conversation_ids)
when 'participating'
@conversations = current_user.participating_conversations.where(account_id: current_account.id)
when 'unattended' when 'unattended'
@conversations = @conversations.where(first_reply_created_at: nil) @conversations = @conversations.where(first_reply_created_at: nil)
end end

View File

@@ -42,6 +42,18 @@ class AgentNotifications::ConversationNotificationsMailer < ApplicationMailer
send_mail_with_liquid(to: @agent.email, subject: subject) and return send_mail_with_liquid(to: @agent.email, subject: subject) and return
end end
def participating_conversation_new_message(message, agent)
return unless smtp_config_set_or_development?
# Don't spam with email notifications if agent is online
return if ::OnlineStatusTracker.get_presence(message.account_id, 'User', agent.id)
@agent = agent
@conversation = message.conversation
subject = "#{@agent.available_name}, New message in your participating conversation [ID - #{@conversation.display_id}]."
@action_url = app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id)
send_mail_with_liquid(to: @agent.email, subject: subject) and return
end
private private
def liquid_droppables def liquid_droppables

View File

@@ -4,7 +4,7 @@ module AssignmentHandler
included do included do
before_save :ensure_assignee_is_from_team before_save :ensure_assignee_is_from_team
after_commit :notify_assignment_change, :process_assignment_activities after_commit :notify_assignment_change, :process_assignment_changes
end end
private private
@@ -36,6 +36,11 @@ module AssignmentHandler
end end
end end
def process_assignment_changes
process_assignment_activities
process_participant_assignment
end
def process_assignment_activities def process_assignment_activities
user_name = Current.user.name if Current.user.present? user_name = Current.user.name if Current.user.present?
if saved_change_to_team_id? if saved_change_to_team_id?
@@ -44,4 +49,10 @@ module AssignmentHandler
create_assignee_change_activity(user_name) create_assignee_change_activity(user_name)
end end
end end
def process_participant_assignment
return unless saved_change_to_assignee_id? && assignee_id.present?
conversation_participants.find_or_create_by!(user_id: assignee_id)
end
end end

View File

@@ -0,0 +1,53 @@
module UserAttributeHelpers
extend ActiveSupport::Concern
def available_name
self[:display_name].presence || name
end
def availability_status
current_account_user&.availability_status
end
def auto_offline
current_account_user&.auto_offline
end
def inviter
current_account_user&.inviter
end
def active_account_user
account_users.order(active_at: :desc)&.first
end
def current_account_user
# We want to avoid subsequent queries in case where the association is preloaded.
# using where here will trigger n+1 queries.
account_users.find { |ac_usr| ac_usr.account_id == Current.account.id } if Current.account
end
def account
current_account_user&.account
end
def administrator?
current_account_user&.administrator?
end
def agent?
current_account_user&.agent?
end
def role
current_account_user&.role
end
# Used internally for Chatwoot in Chatwoot
def hmac_identifier
hmac_key = GlobalConfig.get('CHATWOOT_INBOX_HMAC_KEY')['CHATWOOT_INBOX_HMAC_KEY']
return OpenSSL::HMAC.hexdigest('sha256', hmac_key, email) if hmac_key.present?
''
end
end

View File

@@ -86,6 +86,7 @@ class Conversation < ApplicationRecord
has_many :mentions, dependent: :destroy_async has_many :mentions, dependent: :destroy_async
has_many :messages, dependent: :destroy_async, autosave: true has_many :messages, dependent: :destroy_async, autosave: true
has_one :csat_survey_response, dependent: :destroy_async has_one :csat_survey_response, dependent: :destroy_async
has_many :conversation_participants, dependent: :destroy_async
has_many :notifications, as: :primary_actor, dependent: :destroy_async has_many :notifications, as: :primary_actor, dependent: :destroy_async
before_save :ensure_snooze_until_reset before_save :ensure_snooze_until_reset

View File

@@ -0,0 +1,41 @@
# == Schema Information
#
# Table name: conversation_participants
#
# id :bigint not null, primary key
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# conversation_id :bigint not null
# user_id :bigint not null
#
# Indexes
#
# index_conversation_participants_on_account_id (account_id)
# index_conversation_participants_on_conversation_id (conversation_id)
# index_conversation_participants_on_user_id (user_id)
# index_conversation_participants_on_user_id_and_conversation_id (user_id,conversation_id) UNIQUE
#
class ConversationParticipant < ApplicationRecord
validates :account_id, presence: true
validates :conversation_id, presence: true
validates :user_id, presence: true
validates :user_id, uniqueness: { scope: [:conversation_id] }
validate :ensure_inbox_access
belongs_to :account
belongs_to :conversation
belongs_to :user
before_validation :ensure_account_id
private
def ensure_account_id
self.account_id = conversation&.account_id
end
def ensure_inbox_access
errors.add(:user, 'must have inbox access') if conversation && conversation.inbox.assignable_agents.exclude?(user)
end
end

View File

@@ -34,7 +34,8 @@ class Notification < ApplicationRecord
conversation_creation: 1, conversation_creation: 1,
conversation_assignment: 2, conversation_assignment: 2,
assigned_conversation_new_message: 3, assigned_conversation_new_message: 3,
conversation_mention: 4 conversation_mention: 4,
participating_conversation_new_message: 5
}.freeze }.freeze
enum notification_type: NOTIFICATION_TYPES enum notification_type: NOTIFICATION_TYPES
@@ -94,7 +95,7 @@ class Notification < ApplicationRecord
I18n.t('notifications.notification_title.conversation_creation', display_id: primary_actor.display_id, inbox_name: primary_actor.inbox.name) I18n.t('notifications.notification_title.conversation_creation', display_id: primary_actor.display_id, inbox_name: primary_actor.inbox.name)
when 'conversation_assignment' when 'conversation_assignment'
I18n.t('notifications.notification_title.conversation_assignment', display_id: primary_actor.display_id) I18n.t('notifications.notification_title.conversation_assignment', display_id: primary_actor.display_id)
when 'assigned_conversation_new_message' when 'assigned_conversation_new_message', 'participating_conversation_new_message'
I18n.t( I18n.t(
'notifications.notification_title.assigned_conversation_new_message', 'notifications.notification_title.assigned_conversation_new_message',
display_id: conversation.display_id, display_id: conversation.display_id,
@@ -109,7 +110,11 @@ class Notification < ApplicationRecord
# rubocop:enable Metrics/CyclomaticComplexity # rubocop:enable Metrics/CyclomaticComplexity
def conversation def conversation
return primary_actor.conversation if %w[assigned_conversation_new_message conversation_mention].include? notification_type return primary_actor.conversation if %w[
assigned_conversation_new_message
participating_conversation_new_message
conversation_mention
].include? notification_type
primary_actor primary_actor
end end

View File

@@ -48,6 +48,7 @@ class User < ApplicationRecord
include Rails.application.routes.url_helpers include Rails.application.routes.url_helpers
include Reportable include Reportable
include SsoAuthenticatable include SsoAuthenticatable
include UserAttributeHelpers
devise :database_authenticatable, devise :database_authenticatable,
:registerable, :registerable,
@@ -76,6 +77,8 @@ class User < ApplicationRecord
has_many :assigned_conversations, foreign_key: 'assignee_id', class_name: 'Conversation', dependent: :nullify has_many :assigned_conversations, foreign_key: 'assignee_id', class_name: 'Conversation', dependent: :nullify
alias_attribute :conversations, :assigned_conversations alias_attribute :conversations, :assigned_conversations
has_many :csat_survey_responses, foreign_key: 'assigned_agent_id', dependent: :nullify has_many :csat_survey_responses, foreign_key: 'assigned_agent_id', dependent: :nullify
has_many :conversation_participants, dependent: :destroy_async
has_many :participating_conversations, through: :conversation_participants, source: :conversation
has_many :inbox_members, dependent: :destroy_async has_many :inbox_members, dependent: :destroy_async
has_many :inboxes, through: :inbox_members, source: :inbox has_many :inboxes, through: :inbox_members, source: :inbox
@@ -110,60 +113,10 @@ class User < ApplicationRecord
self.uid = email self.uid = email
end end
def active_account_user
account_users.order(active_at: :desc)&.first
end
def current_account_user
# We want to avoid subsequent queries in case where the association is preloaded.
# using where here will trigger n+1 queries.
account_users.find { |ac_usr| ac_usr.account_id == Current.account.id } if Current.account
end
def available_name
self[:display_name].presence || name
end
# Used internally for Chatwoot in Chatwoot
def hmac_identifier
hmac_key = GlobalConfig.get('CHATWOOT_INBOX_HMAC_KEY')['CHATWOOT_INBOX_HMAC_KEY']
return OpenSSL::HMAC.hexdigest('sha256', hmac_key, email) if hmac_key.present?
''
end
def account
current_account_user&.account
end
def assigned_inboxes def assigned_inboxes
administrator? ? Current.account.inboxes : inboxes.where(account_id: Current.account.id) administrator? ? Current.account.inboxes : inboxes.where(account_id: Current.account.id)
end end
def administrator?
current_account_user&.administrator?
end
def agent?
current_account_user&.agent?
end
def role
current_account_user&.role
end
def availability_status
current_account_user&.availability_status
end
def auto_offline
current_account_user&.auto_offline
end
def inviter
current_account_user&.inviter
end
def serializable_hash(options = nil) def serializable_hash(options = nil)
super(options).merge(confirmed: confirmed?) super(options).merge(confirmed: confirmed?)
end end

View File

@@ -9,6 +9,7 @@ class Messages::MentionService
Conversations::UserMentionJob.perform_later(validated_mentioned_ids, message.conversation.id, message.account.id) Conversations::UserMentionJob.perform_later(validated_mentioned_ids, message.conversation.id, message.account.id)
generate_notifications_for_mentions(validated_mentioned_ids) generate_notifications_for_mentions(validated_mentioned_ids)
add_mentioned_users_as_participants(validated_mentioned_ids)
end end
private private
@@ -38,4 +39,10 @@ class Messages::MentionService
).perform ).perform
end end
end end
def add_mentioned_users_as_participants(validated_mentioned_ids)
validated_mentioned_ids.each do |user_id|
message.conversation.conversation_participants.find_or_create_by!(user_id: user_id)
end
end
end end

View File

@@ -4,6 +4,7 @@ class Messages::NewMessageNotificationService
def perform def perform
return unless message.notifiable? return unless message.notifiable?
notify_participating_users
notify_conversation_assignee notify_conversation_assignee
end end
@@ -11,8 +12,23 @@ class Messages::NewMessageNotificationService
delegate :conversation, :sender, :account, to: :message delegate :conversation, :sender, :account, to: :message
def notify_participating_users
participating_users = conversation.conversation_participants.map(&:user)
participating_users -= [sender] if sender.is_a?(User)
participating_users.uniq.each do |participant|
NotificationBuilder.new(
notification_type: 'participating_conversation_new_message',
user: participant,
account: account,
primary_actor: message
).perform
end
end
def notify_conversation_assignee def notify_conversation_assignee
return if conversation.assignee.blank? return if conversation.assignee.blank?
return if assignee_already_notified_via_participation?
return if conversation.assignee == sender return if conversation.assignee == sender
NotificationBuilder.new( NotificationBuilder.new(
@@ -22,4 +38,13 @@ class Messages::NewMessageNotificationService
primary_actor: message primary_actor: message
).perform ).perform
end end
def assignee_already_notified_via_participation?
return unless conversation.conversation_participants.map(&:user).include?(conversation.assignee)
# check whether participation notifcation is disabled for assignee
notification_setting = conversation.assignee.notification_settings.find_by(account_id: account.id)
notification_setting.public_send(:email_participating_conversation_new_message?) || notification_setting
.public_send(:push_participating_conversation_new_message?)
end
end end

View File

@@ -0,0 +1,3 @@
json.array! @participants do |participant|
json.partial! 'api/v1/models/agent', format: :json, resource: participant.user
end

View File

@@ -0,0 +1,3 @@
json.array! @participants do |participant|
json.partial! 'api/v1/models/agent', format: :json, resource: participant.user
end

View File

@@ -11,7 +11,11 @@ json.data do
json.notification_type notification.notification_type json.notification_type notification.notification_type
json.push_message_title notification.push_message_title json.push_message_title notification.push_message_title
# TODO: front end assumes primary actor to be conversation. should fix in future # TODO: front end assumes primary actor to be conversation. should fix in future
if %w[assigned_conversation_new_message conversation_mention].include? notification.notification_type if %w[
assigned_conversation_new_message
participating_conversation_new_message
conversation_mention
].include? notification.notification_type
json.primary_actor_type 'Conversation' json.primary_actor_type 'Conversation'
json.primary_actor_id notification.conversation.id json.primary_actor_id notification.conversation.id
json.primary_actor notification.conversation.push_event_data json.primary_actor notification.conversation.push_event_data

View File

@@ -0,0 +1,5 @@
<p>Hi {{user.available_name}},</p>
<p>You have received a new message in a conversation you are participating.</p>
<p>Click <a href="{{action_url}}">here</a> to get cracking.</p>

View File

@@ -77,6 +77,7 @@ Rails.application.routes.draw do
resources :messages, only: [:index, :create, :destroy] resources :messages, only: [:index, :create, :destroy]
resources :assignments, only: [:create] resources :assignments, only: [:create]
resources :labels, only: [:create, :index] resources :labels, only: [:create, :index]
resource :participants, only: [:show, :create, :update, :destroy]
resource :direct_uploads, only: [:create] resource :direct_uploads, only: [:create]
end end
member do member do

View File

@@ -0,0 +1,11 @@
class AddConversationParticipants < ActiveRecord::Migration[6.1]
def change
create_table 'conversation_participants', force: :cascade do |t|
t.references :account, null: false
t.references :user, null: false
t.references :conversation, null: false
t.datetime 'created_at', precision: 6, null: false
t.datetime 'updated_at', precision: 6, null: false
end
end
end

View File

@@ -0,0 +1,5 @@
class AddUniqueIndexToConversationParticipants < ActiveRecord::Migration[6.1]
def change
add_index :conversation_participants, [:user_id, :conversation_id], unique: true
end
end

View File

@@ -388,6 +388,18 @@ ActiveRecord::Schema.define(version: 2023_02_09_033203) do
t.index ["phone_number", "account_id"], name: "index_contacts_on_phone_number_and_account_id" t.index ["phone_number", "account_id"], name: "index_contacts_on_phone_number_and_account_id"
end end
create_table "conversation_participants", force: :cascade do |t|
t.bigint "account_id", null: false
t.bigint "user_id", null: false
t.bigint "conversation_id", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["account_id"], name: "index_conversation_participants_on_account_id"
t.index ["conversation_id"], name: "index_conversation_participants_on_conversation_id"
t.index ["user_id", "conversation_id"], name: "index_conversation_participants_on_user_id_and_conversation_id", unique: true
t.index ["user_id"], name: "index_conversation_participants_on_user_id"
end
create_table "conversations", id: :serial, force: :cascade do |t| create_table "conversations", id: :serial, force: :cascade do |t|
t.integer "account_id", null: false t.integer "account_id", null: false
t.integer "inbox_id", null: false t.integer "inbox_id", null: false

View File

@@ -12,6 +12,10 @@ RSpec.describe 'Api::V1::Accounts::BulkActionsController', type: :request do
create(:conversation, account_id: account.id, status: :open, team_id: team_1.id) create(:conversation, account_id: account.id, status: :open, team_id: team_1.id)
create(:conversation, account_id: account.id, status: :open) create(:conversation, account_id: account.id, status: :open)
create(:conversation, account_id: account.id, status: :open) create(:conversation, account_id: account.id, status: :open)
Conversation.all.each do |conversation|
create(:inbox_member, inbox: conversation.inbox, user: agent_1)
create(:inbox_member, inbox: conversation.inbox, user: agent_2)
end
end end
describe 'POST /api/v1/accounts/{account.id}/bulk_action' do describe 'POST /api/v1/accounts/{account.id}/bulk_action' do

View File

@@ -56,8 +56,8 @@ RSpec.describe 'Conversation Assignment API', type: :request do
let(:agent) { create(:user, account: account, role: :agent) } let(:agent) { create(:user, account: account, role: :agent) }
before do before do
conversation.update!(assignee: agent)
create(:inbox_member, inbox: conversation.inbox, user: agent) create(:inbox_member, inbox: conversation.inbox, user: agent)
conversation.update!(assignee: agent)
end end
it 'unassigns the assignee from the conversation' do it 'unassigns the assignee from the conversation' do

View File

@@ -0,0 +1,142 @@
require 'rails_helper'
RSpec.describe 'Conversation Participants API', type: :request do
let(:account) { create(:account) }
let(:conversation) { create(:conversation, account: account) }
let(:agent) { create(:user, account: account, role: :agent) }
before do
create(:inbox_member, inbox: conversation.inbox, user: agent)
end
describe 'GET /api/v1/accounts/{account.id}/conversations/<id>/paricipants' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get api_v1_account_conversation_participants_url(account_id: account.id, conversation_id: conversation.display_id)
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user with access to the conversation' do
let(:participant1) { create(:user, account: account, role: :agent) }
let(:participant2) { create(:user, account: account, role: :agent) }
before do
create(:inbox_member, inbox: conversation.inbox, user: participant1)
create(:inbox_member, inbox: conversation.inbox, user: participant2)
end
it 'returns all the partipants for the conversation' do
create(:conversation_participant, conversation: conversation, user: participant1)
create(:conversation_participant, conversation: conversation, user: participant2)
get api_v1_account_conversation_participants_url(account_id: account.id, conversation_id: conversation.display_id),
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include(participant1.email)
expect(response.body).to include(participant2.email)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/conversations/<id>/participants' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post api_v1_account_conversation_participants_url(account_id: account.id, conversation_id: conversation.display_id)
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:participant) { create(:user, account: account, role: :agent) }
before do
create(:inbox_member, inbox: conversation.inbox, user: participant)
end
it 'creates a new participants when its authorized agent' do
params = { user_ids: [participant.id] }
expect(conversation.conversation_participants.count).to eq(0)
post api_v1_account_conversation_participants_url(account_id: account.id, conversation_id: conversation.display_id),
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include(participant.email)
expect(conversation.conversation_participants.count).to eq(1)
end
end
end
describe 'PUT /api/v1/accounts/{account.id}/conversations/<id>/participants' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
put api_v1_account_conversation_participants_url(account_id: account.id, conversation_id: conversation.display_id)
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:participant) { create(:user, account: account, role: :agent) }
let(:participant_to_be_added) { create(:user, account: account, role: :agent) }
let(:participant_to_be_removed) { create(:user, account: account, role: :agent) }
before do
create(:inbox_member, inbox: conversation.inbox, user: participant)
create(:inbox_member, inbox: conversation.inbox, user: participant_to_be_added)
create(:inbox_member, inbox: conversation.inbox, user: participant_to_be_removed)
end
it 'updates participants when its authorized agent' do
params = { user_ids: [participant.id, participant_to_be_added.id] }
create(:conversation_participant, conversation: conversation, user: participant)
create(:conversation_participant, conversation: conversation, user: participant_to_be_removed)
expect(conversation.conversation_participants.count).to eq(2)
put api_v1_account_conversation_participants_url(account_id: account.id, conversation_id: conversation.display_id),
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include(participant.email)
expect(response.body).to include(participant_to_be_added.email)
expect(conversation.conversation_participants.count).to eq(2)
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/conversations/<id>/participants' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
delete api_v1_account_conversation_participants_url(account_id: account.id, conversation_id: conversation.display_id)
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:participant) { create(:user, account: account, role: :agent) }
before do
create(:inbox_member, inbox: conversation.inbox, user: participant)
end
it 'deletes participants when its authorized agent' do
params = { user_ids: [participant.id] }
create(:conversation_participant, conversation: conversation, user: participant)
expect(conversation.conversation_participants.count).to eq(1)
delete api_v1_account_conversation_participants_url(account_id: account.id, conversation_id: conversation.display_id),
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(conversation.conversation_participants.count).to eq(0)
end
end
end
end

View File

@@ -111,11 +111,11 @@ RSpec.describe 'Reports API', type: :request do
context 'when an agent1 associated to conversation having first reply from agent2' do context 'when an agent1 associated to conversation having first reply from agent2' do
let(:listener) { ReportingEventListener.instance } let(:listener) { ReportingEventListener.instance }
let(:account) { create(:account) } let(:account) { create(:account) }
let(:admin) { create(:user, account: account, role: :administrator) } let(:agent2) { create(:user, account: account, role: :agent) }
it 'returns unattended conversation count zero for agent1' do it 'returns unattended conversation count zero for agent1' do
agent1 = create(:user, account: account, role: :agent) create(:inbox_member, user: agent, inbox: inbox)
agent2 = create(:user, account: account, role: :agent) create(:inbox_member, user: agent2, inbox: inbox)
conversation = create(:conversation, account: account, conversation = create(:conversation, account: account,
inbox: inbox, assignee: agent2) inbox: inbox, assignee: agent2)
@@ -129,7 +129,7 @@ RSpec.describe 'Reports API', type: :request do
event = Events::Base.new('first.reply.created', Time.zone.now, message: first_reply_message) event = Events::Base.new('first.reply.created', Time.zone.now, message: first_reply_message)
listener.first_reply_created(event) listener.first_reply_created(event)
conversation.assignee_id = agent1.id conversation.assignee_id = agent.id
conversation.save! conversation.save!
get "/api/v2/accounts/#{account.id}/reports/conversations", get "/api/v2/accounts/#{account.id}/reports/conversations",
@@ -140,7 +140,7 @@ RSpec.describe 'Reports API', type: :request do
as: :json as: :json
json_response = JSON.parse(response.body) json_response = JSON.parse(response.body)
user_metrics = json_response.find { |item| item['name'] == agent1[:name] } user_metrics = json_response.find { |item| item['name'] == agent[:name] }
expect(user_metrics.present?).to be true expect(user_metrics.present?).to be true
expect(user_metrics['metric']['open']).to eq(1) expect(user_metrics['metric']['open']).to eq(1)

View File

@@ -0,0 +1,13 @@
FactoryBot.define do
factory :conversation_participant do
conversation
account
before(:build) do |conversation|
if conversation.user.blank?
conversation.user = create(:user, account: conversation.account)
create(:inbox_member, user: conversation.user, inbox: conversation.conversation.inbox)
end
end
end
end

View File

@@ -15,6 +15,12 @@ RSpec.describe BulkActionsJob, type: :job do
let!(:conversation_2) { create(:conversation, account_id: account.id, status: :open) } let!(:conversation_2) { create(:conversation, account_id: account.id, status: :open) }
let!(:conversation_3) { create(:conversation, account_id: account.id, status: :open) } let!(:conversation_3) { create(:conversation, account_id: account.id, status: :open) }
before do
Conversation.all.each do |conversation|
create(:inbox_member, inbox: conversation.inbox, user: agent)
end
end
it 'enqueues the job' do it 'enqueues the job' do
expect { job }.to have_enqueued_job(described_class) expect { job }.to have_enqueued_job(described_class)
.with(account: account, params: params, user: agent) .with(account: account, params: params, user: agent)

View File

@@ -87,4 +87,22 @@ RSpec.describe AgentNotifications::ConversationNotificationsMailer, type: :maile
expect(mail).to be_nil expect(mail).to be_nil
end end
end end
describe 'participating_conversation_new_message' do
let(:message) { create(:message, conversation: conversation, account: account) }
let(:mail) { described_class.with(account: account).participating_conversation_new_message(message, agent).deliver_now }
it 'renders the subject' do
expect(mail.subject).to eq("#{agent.available_name}, New message in your participating conversation [ID - #{message.conversation.display_id}].")
end
it 'renders the receiver email' do
expect(mail.to).to eq([agent.email])
end
it 'will not send email if agent is online' do
::OnlineStatusTracker.update_presence(conversation.account.id, 'User', agent.id)
expect(mail).to be_nil
end
end
end end

View File

@@ -73,6 +73,10 @@ shared_examples_for 'assignment_handler' do
end end
let(:assignment_mailer) { instance_double(AgentNotifications::ConversationNotificationsMailer, deliver: true) } let(:assignment_mailer) { instance_double(AgentNotifications::ConversationNotificationsMailer, deliver: true) }
before do
create(:inbox_member, user: agent, inbox: conversation.inbox)
end
it 'assigns the agent to conversation' do it 'assigns the agent to conversation' do
expect(update_assignee).to be(true) expect(update_assignee).to be(true)
expect(conversation.reload.assignee).to eq(agent) expect(conversation.reload.assignee).to eq(agent)
@@ -85,6 +89,10 @@ shared_examples_for 'assignment_handler' do
expect(update_assignee).to be(true) expect(update_assignee).to be(true)
end end
it 'adds assignee to conversation participants' do
expect { update_assignee }.to change { conversation.conversation_participants.count }.by(1)
end
context 'when agent is current user' do context 'when agent is current user' do
before do before do
Current.user = agent Current.user = agent

View File

@@ -0,0 +1,32 @@
require 'rails_helper'
RSpec.describe ConversationParticipant, type: :model do
context 'with validations' do
it { is_expected.to validate_presence_of(:account_id) }
it { is_expected.to validate_presence_of(:conversation_id) }
it { is_expected.to validate_presence_of(:user_id) }
end
describe 'associations' do
it { is_expected.to belong_to(:account) }
it { is_expected.to belong_to(:conversation) }
it { is_expected.to belong_to(:user) }
end
describe 'validations' do
it 'ensure account is present' do
conversation = create(:conversation)
conversation_participant = build(:conversation_participant, conversation: conversation, account_id: nil)
conversation_participant.valid?
expect(conversation_participant.account_id).to eq(conversation.account_id)
end
it 'throws error if inbox member does not belongs to account' do
conversation = create(:conversation)
user = create(:user, account: conversation.account)
participant = build(:conversation_participant, user: user, conversation: conversation)
expect { participant.save! }.to raise_error(ActiveRecord::RecordInvalid)
expect(participant.errors.messages[:user]).to eq(['must have inbox access'])
end
end
end

View File

@@ -105,6 +105,8 @@ RSpec.describe Conversation, type: :model do
let(:label) { create(:label, account: account) } let(:label) { create(:label, account: account) }
before do 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) allow(Rails.configuration.dispatcher).to receive(:dispatch)
Current.user = old_assignee Current.user = old_assignee
end end

View File

@@ -51,6 +51,14 @@ RSpec.describe Notification do
expect(notification.push_message_title).to eq "[New message] - ##{notification.conversation.display_id} " expect(notification.push_message_title).to eq "[New message] - ##{notification.conversation.display_id} "
end end
it 'returns appropriate title suited for the notification type participating_conversation_new_message' do
message = create(:message, sender: create(:user), content: Faker::Lorem.paragraphs(number: 2))
notification = create(:notification, notification_type: 'participating_conversation_new_message', primary_actor: message)
expect(notification.push_message_title).to eq "[New message] - ##{notification.conversation.display_id} \
#{message.content.truncate_words(10)}"
end
it 'returns appropriate title suited for the notification type conversation_mention' do it 'returns appropriate title suited for the notification type conversation_mention' do
message = create(:message, sender: create(:user), content: 'Hey [@John](mention://user/1/john), can you check this ticket?') message = create(:message, sender: create(:user), content: 'Hey [@John](mention://user/1/john), can you check this ticket?')
notification = create(:notification, notification_type: 'conversation_mention', primary_actor: message, secondary_actor: message.sender) notification = create(:notification, notification_type: 'conversation_mention', primary_actor: message, secondary_actor: message.sender)

View File

@@ -16,6 +16,7 @@ RSpec.describe AutoAssignment::AgentAssignmentService do
end end
before do before do
inbox_members.each { |inbox_member| create(:account_user, account: account, user: inbox_member.user) }
allow(::OnlineStatusTracker).to receive(:get_available_users).and_return(online_users) allow(::OnlineStatusTracker).to receive(:get_available_users).and_return(online_users)
end end

View File

@@ -61,5 +61,10 @@ describe Messages::MentionService do
account: account, account: account,
primary_actor: message) primary_actor: message)
end end
it 'add the users to the participants list' do
described_class.new(message: message).perform
expect(conversation.conversation_participants.map(&:user_id)).to match_array([first_agent.id, second_agent.id])
end
end end
end end

View File

@@ -13,13 +13,18 @@ describe Messages::NewMessageNotificationService do
let(:account) { create(:account) } let(:account) { create(:account) }
let(:assignee) { create(:user, account: account) } let(:assignee) { create(:user, account: account) }
let(:participating_agent_1) { create(:user, account: account) } let(:participating_agent_1) { create(:user, account: account) }
let(:participating_agent_2) { create(:user, account: account) }
let(:inbox) { create(:inbox, account: account) } let(:inbox) { create(:inbox, account: account) }
let(:conversation) { create(:conversation, account: account, inbox: inbox, assignee: assignee) } let(:conversation) { create(:conversation, account: account, inbox: inbox, assignee: assignee) }
let(:builder) { double } let(:builder) { double }
before do before do
create(:inbox_member, inbox: inbox, user: participating_agent_1) create(:inbox_member, inbox: inbox, user: participating_agent_1)
create(:inbox_member, inbox: inbox, user: participating_agent_2)
create(:inbox_member, inbox: inbox, user: assignee) create(:inbox_member, inbox: inbox, user: assignee)
create(:conversation_participant, conversation: conversation, user: participating_agent_1)
create(:conversation_participant, conversation: conversation, user: participating_agent_2)
create(:conversation_participant, conversation: conversation, user: assignee)
allow(NotificationBuilder).to receive(:new).and_return(builder) allow(NotificationBuilder).to receive(:new).and_return(builder)
allow(builder).to receive(:perform) allow(builder).to receive(:perform)
end end
@@ -31,12 +36,26 @@ describe Messages::NewMessageNotificationService do
described_class.new(message: message).perform described_class.new(message: message).perform
end end
it 'creates notifications for other participating users' do
expect(NotificationBuilder).to have_received(:new).with(notification_type: 'participating_conversation_new_message',
user: participating_agent_2,
account: account,
primary_actor: message)
end
it 'creates notifications for assignee' do it 'creates notifications for assignee' do
expect(NotificationBuilder).to have_received(:new).with(notification_type: 'assigned_conversation_new_message', expect(NotificationBuilder).to have_received(:new).with(notification_type: 'assigned_conversation_new_message',
user: assignee, user: assignee,
account: account, account: account,
primary_actor: message) primary_actor: message)
end end
it 'will not create notifications for the user who created the message' do
expect(NotificationBuilder).not_to have_received(:new).with(notification_type: 'participating_conversation_new_message',
user: participating_agent_1,
account: account,
primary_actor: message)
end
end end
context 'when message is created by a contact' do context 'when message is created by a contact' do
@@ -52,6 +71,34 @@ describe Messages::NewMessageNotificationService do
account: account, account: account,
primary_actor: message) primary_actor: message)
end end
it 'creates notifications for all participating users' do
expect(NotificationBuilder).to have_received(:new).with(notification_type: 'participating_conversation_new_message',
user: participating_agent_1,
account: account,
primary_actor: message)
expect(NotificationBuilder).to have_received(:new).with(notification_type: 'participating_conversation_new_message',
user: participating_agent_2,
account: account,
primary_actor: message)
end
end
context 'with multiple notifications are subscribed' do
let(:message) { create(:message, conversation: conversation, account: account) }
before do
assignee.notification_settings.find_by(account_id: account.id).update(selected_email_flags: %w[email_assigned_conversation_new_message
email_participating_conversation_new_message])
described_class.new(message: message).perform
end
it 'will not create assignee notifications for the assignee if participating notification was send' do
expect(NotificationBuilder).not_to have_received(:new).with(notification_type: 'assigned_conversation_new_message',
user: assignee,
account: account,
primary_actor: message)
end
end end
context 'when message is created by assignee' do context 'when message is created by assignee' do
@@ -62,6 +109,10 @@ describe Messages::NewMessageNotificationService do
end end
it 'will not create notifications for the user who created the message' do it 'will not create notifications for the user who created the message' do
expect(NotificationBuilder).not_to have_received(:new).with(notification_type: 'participating_conversation_new_message',
user: assignee,
account: account,
primary_actor: message)
expect(NotificationBuilder).not_to have_received(:new).with(notification_type: 'assigned_conversation_new_message', expect(NotificationBuilder).not_to have_received(:new).with(notification_type: 'assigned_conversation_new_message',
user: assignee, user: assignee,
account: account, account: account,