feat: add push notification when SLA missed (#9078)
* feat: add push notification when SLA missed * chore: sent notification only for inbox members * feat: add conv particpants+admins to SLA notification list * chore: add spec to ensure notification is created * chore: refactor to multiple alerts for SLA conditions * chore: add sla_policy as secondary_actor in notification
This commit is contained in:
parent
0685e04aae
commit
aaf70cf1cf
@ -87,7 +87,10 @@
|
|||||||
"conversation_assignment": "Conversation Assigned",
|
"conversation_assignment": "Conversation Assigned",
|
||||||
"assigned_conversation_new_message": "New Message",
|
"assigned_conversation_new_message": "New Message",
|
||||||
"participating_conversation_new_message": "New Message",
|
"participating_conversation_new_message": "New Message",
|
||||||
"conversation_mention": "Mention"
|
"conversation_mention": "Mention",
|
||||||
|
"sla_missed_first_response": "SLA Missed",
|
||||||
|
"sla_missed_next_response": "SLA Missed",
|
||||||
|
"sla_missed_resolution": "SLA Missed"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"NETWORK": {
|
"NETWORK": {
|
||||||
|
|||||||
@ -22,7 +22,10 @@
|
|||||||
"CONVERSATION_CREATION": "New conversation created",
|
"CONVERSATION_CREATION": "New conversation created",
|
||||||
"CONVERSATION_ASSIGNMENT": "A conversation has been assigned to you",
|
"CONVERSATION_ASSIGNMENT": "A conversation has been assigned to you",
|
||||||
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "New message in an assigned conversation",
|
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "New message in an assigned conversation",
|
||||||
"PARTICIPATING_CONVERSATION_NEW_MESSAGE": "New message in a conversation you are participating in"
|
"PARTICIPATING_CONVERSATION_NEW_MESSAGE": "New message in a conversation you are participating in",
|
||||||
|
"SLA_MISSED_FIRST_RESPONSE": "SLA target first response missed for conversation",
|
||||||
|
"SLA_MISSED_NEXT_RESPONSE": "SLA target next response missed for conversation",
|
||||||
|
"SLA_MISSED_RESOLUTION": "SLA target resolution missed for conversation"
|
||||||
},
|
},
|
||||||
"MENU_ITEM": {
|
"MENU_ITEM": {
|
||||||
"MARK_AS_READ": "Mark as read",
|
"MARK_AS_READ": "Mark as read",
|
||||||
|
|||||||
@ -39,7 +39,10 @@ class Notification < ApplicationRecord
|
|||||||
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
|
participating_conversation_new_message: 5,
|
||||||
|
sla_missed_first_response: 6,
|
||||||
|
sla_missed_next_response: 7,
|
||||||
|
sla_missed_resolution: 8
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
enum notification_type: NOTIFICATION_TYPES
|
enum notification_type: NOTIFICATION_TYPES
|
||||||
@ -86,21 +89,32 @@ class Notification < ApplicationRecord
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO: move to a data presenter
|
# rubocop:disable Metrics/MethodLength
|
||||||
def push_message_title
|
def push_message_title
|
||||||
case notification_type
|
notification_title_map = {
|
||||||
when 'conversation_creation'
|
'conversation_creation' => 'notifications.notification_title.conversation_creation',
|
||||||
I18n.t('notifications.notification_title.conversation_creation', display_id: conversation.display_id, inbox_name: primary_actor.inbox.name)
|
'conversation_assignment' => 'notifications.notification_title.conversation_assignment',
|
||||||
when 'conversation_assignment'
|
'assigned_conversation_new_message' => 'notifications.notification_title.assigned_conversation_new_message',
|
||||||
I18n.t('notifications.notification_title.conversation_assignment', display_id: conversation.display_id)
|
'participating_conversation_new_message' => 'notifications.notification_title.assigned_conversation_new_message',
|
||||||
when 'assigned_conversation_new_message', 'participating_conversation_new_message'
|
'conversation_mention' => 'notifications.notification_title.conversation_mention',
|
||||||
I18n.t('notifications.notification_title.assigned_conversation_new_message', display_id: conversation.display_id)
|
'sla_missed_first_response' => 'notifications.notification_title.sla_missed_first_response',
|
||||||
when 'conversation_mention'
|
'sla_missed_next_response' => 'notifications.notification_title.sla_missed_next_response',
|
||||||
I18n.t('notifications.notification_title.conversation_mention', display_id: conversation.display_id)
|
'sla_missed_resolution' => 'notifications.notification_title.sla_missed_resolution'
|
||||||
|
}
|
||||||
|
|
||||||
|
i18n_key = notification_title_map[notification_type]
|
||||||
|
return '' unless i18n_key
|
||||||
|
|
||||||
|
if notification_type == 'conversation_creation'
|
||||||
|
I18n.t(i18n_key, display_id: conversation.display_id, inbox_name: primary_actor.inbox.name)
|
||||||
|
elsif %w[conversation_assignment assigned_conversation_new_message participating_conversation_new_message
|
||||||
|
conversation_mention].include?(notification_type)
|
||||||
|
I18n.t(i18n_key, display_id: conversation.display_id)
|
||||||
else
|
else
|
||||||
''
|
I18n.t(i18n_key, display_id: primary_actor.display_id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
# rubocop:enable Metrics/MethodLength
|
||||||
|
|
||||||
def push_message_body
|
def push_message_body
|
||||||
case notification_type
|
case notification_type
|
||||||
|
|||||||
@ -122,6 +122,9 @@ en:
|
|||||||
conversation_assignment: "A conversation (#%{display_id}) has been assigned to you"
|
conversation_assignment: "A conversation (#%{display_id}) has been assigned to you"
|
||||||
assigned_conversation_new_message: "A new message is created in conversation (#%{display_id})"
|
assigned_conversation_new_message: "A new message is created in conversation (#%{display_id})"
|
||||||
conversation_mention: "You have been mentioned in conversation (#%{display_id})"
|
conversation_mention: "You have been mentioned in conversation (#%{display_id})"
|
||||||
|
sla_missed_first_response: "SLA target first response missed for conversation (#%{display_id})"
|
||||||
|
sla_missed_next_response: "SLA target next response missed for conversation (#%{display_id})"
|
||||||
|
sla_missed_resolution: "SLA target resolution missed for conversation (#%{display_id})"
|
||||||
attachment: "Attachment"
|
attachment: "Attachment"
|
||||||
no_content: "No content"
|
no_content: "No content"
|
||||||
conversations:
|
conversations:
|
||||||
|
|||||||
@ -22,4 +22,14 @@ class SlaPolicy < ApplicationRecord
|
|||||||
validates :name, presence: true
|
validates :name, presence: true
|
||||||
|
|
||||||
has_many :conversations, dependent: :nullify
|
has_many :conversations, dependent: :nullify
|
||||||
|
|
||||||
|
def push_event_data
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
frt: first_response_time_threshold,
|
||||||
|
nrt: next_response_time_threshold,
|
||||||
|
rt: resolution_time_threshold
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -30,7 +30,7 @@ class Sla::EvaluateAppliedSlaService
|
|||||||
return if first_reply_was_within_threshold?(conversation, threshold)
|
return if first_reply_was_within_threshold?(conversation, threshold)
|
||||||
return if still_within_threshold?(threshold)
|
return if still_within_threshold?(threshold)
|
||||||
|
|
||||||
handle_missed_sla(applied_sla)
|
handle_missed_sla(applied_sla, 'frt')
|
||||||
end
|
end
|
||||||
|
|
||||||
def first_reply_was_within_threshold?(conversation, threshold)
|
def first_reply_was_within_threshold?(conversation, threshold)
|
||||||
@ -46,7 +46,7 @@ class Sla::EvaluateAppliedSlaService
|
|||||||
threshold = conversation.waiting_since.to_i + sla_policy.next_response_time_threshold.to_i
|
threshold = conversation.waiting_since.to_i + sla_policy.next_response_time_threshold.to_i
|
||||||
return if still_within_threshold?(threshold)
|
return if still_within_threshold?(threshold)
|
||||||
|
|
||||||
handle_missed_sla(applied_sla)
|
handle_missed_sla(applied_sla, 'nrt')
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_resolution_time_threshold(applied_sla, conversation, sla_policy)
|
def check_resolution_time_threshold(applied_sla, conversation, sla_policy)
|
||||||
@ -55,13 +55,14 @@ class Sla::EvaluateAppliedSlaService
|
|||||||
threshold = conversation.created_at.to_i + sla_policy.resolution_time_threshold.to_i
|
threshold = conversation.created_at.to_i + sla_policy.resolution_time_threshold.to_i
|
||||||
return if still_within_threshold?(threshold)
|
return if still_within_threshold?(threshold)
|
||||||
|
|
||||||
handle_missed_sla(applied_sla)
|
handle_missed_sla(applied_sla, 'rt')
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_missed_sla(applied_sla)
|
def handle_missed_sla(applied_sla, type)
|
||||||
return unless applied_sla.active?
|
return unless applied_sla.active?
|
||||||
|
|
||||||
applied_sla.update!(sla_status: 'missed')
|
applied_sla.update!(sla_status: 'missed')
|
||||||
|
generate_notifications_for_sla(applied_sla, type)
|
||||||
Rails.logger.warn "SLA missed for conversation #{applied_sla.conversation.id} " \
|
Rails.logger.warn "SLA missed for conversation #{applied_sla.conversation.id} " \
|
||||||
"in account #{applied_sla.account_id} " \
|
"in account #{applied_sla.account_id} " \
|
||||||
"for sla_policy #{applied_sla.sla_policy.id}"
|
"for sla_policy #{applied_sla.sla_policy.id}"
|
||||||
@ -75,4 +76,30 @@ class Sla::EvaluateAppliedSlaService
|
|||||||
"in account #{applied_sla.account_id} " \
|
"in account #{applied_sla.account_id} " \
|
||||||
"for sla_policy #{applied_sla.sla_policy.id}"
|
"for sla_policy #{applied_sla.sla_policy.id}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def generate_notifications_for_sla(applied_sla, type)
|
||||||
|
notify_users = applied_sla.conversation.conversation_participants.map(&:user)
|
||||||
|
# add all admins from the account to notify list
|
||||||
|
notify_users += applied_sla.account.administrators
|
||||||
|
# ensure conversation assignee is notified
|
||||||
|
notify_users += [applied_sla.conversation.assignee] if applied_sla.conversation.assignee.present?
|
||||||
|
|
||||||
|
notification_type = if type == 'frt'
|
||||||
|
'sla_missed_first_response'
|
||||||
|
elsif type == 'nrt'
|
||||||
|
'sla_missed_next_response'
|
||||||
|
else
|
||||||
|
'sla_missed_resolution'
|
||||||
|
end
|
||||||
|
|
||||||
|
notify_users.uniq.each do |user|
|
||||||
|
NotificationBuilder.new(
|
||||||
|
notification_type: notification_type,
|
||||||
|
user: user,
|
||||||
|
account: applied_sla.account,
|
||||||
|
primary_actor: applied_sla.conversation,
|
||||||
|
secondary_actor: applied_sla.sla_policy
|
||||||
|
).perform
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,7 +1,11 @@
|
|||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe Sla::EvaluateAppliedSlaService do
|
RSpec.describe Sla::EvaluateAppliedSlaService do
|
||||||
let!(:conversation) { create(:conversation, created_at: 6.hours.ago) }
|
let!(:account) { create(:account) }
|
||||||
|
let!(:user_1) { create(:user, account: account) }
|
||||||
|
let!(:user_2) { create(:user, account: account) }
|
||||||
|
let!(:admin) { create(:user, account: account, role: :administrator) }
|
||||||
|
let!(:conversation) { create(:conversation, created_at: 6.hours.ago, assignee: user_1, account: account) }
|
||||||
let!(:sla_policy) do
|
let!(:sla_policy) do
|
||||||
create(:sla_policy, account: conversation.account,
|
create(:sla_policy, account: conversation.account,
|
||||||
first_response_time_threshold: nil,
|
first_response_time_threshold: nil,
|
||||||
@ -20,6 +24,16 @@ RSpec.describe Sla::EvaluateAppliedSlaService do
|
|||||||
expect(Rails.logger).to have_received(:warn).with("SLA missed for conversation #{conversation.id} in account " \
|
expect(Rails.logger).to have_received(:warn).with("SLA missed for conversation #{conversation.id} in account " \
|
||||||
"#{applied_sla.account_id} for sla_policy #{sla_policy.id}")
|
"#{applied_sla.account_id} for sla_policy #{sla_policy.id}")
|
||||||
expect(applied_sla.reload.sla_status).to eq('missed')
|
expect(applied_sla.reload.sla_status).to eq('missed')
|
||||||
|
|
||||||
|
expect(Notification.count).to eq(2)
|
||||||
|
# check if notification type is sla_missed_first_response
|
||||||
|
expect(Notification.where(notification_type: 'sla_missed_first_response').count).to eq(2)
|
||||||
|
# Check if notification is created for the assignee
|
||||||
|
expect(Notification.where(user_id: user_1.id).count).to eq(1)
|
||||||
|
# Check if notification is created for the account admin
|
||||||
|
expect(Notification.where(user_id: admin.id).count).to eq(1)
|
||||||
|
# Check if no notification is created for other user
|
||||||
|
expect(Notification.where(user_id: user_2.id).count).to eq(0)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -35,6 +49,16 @@ RSpec.describe Sla::EvaluateAppliedSlaService do
|
|||||||
expect(Rails.logger).to have_received(:warn).with("SLA missed for conversation #{conversation.id} in account " \
|
expect(Rails.logger).to have_received(:warn).with("SLA missed for conversation #{conversation.id} in account " \
|
||||||
"#{applied_sla.account_id} for sla_policy #{sla_policy.id}")
|
"#{applied_sla.account_id} for sla_policy #{sla_policy.id}")
|
||||||
expect(applied_sla.reload.sla_status).to eq('missed')
|
expect(applied_sla.reload.sla_status).to eq('missed')
|
||||||
|
|
||||||
|
expect(Notification.count).to eq(2)
|
||||||
|
# check if notification type is sla_missed_first_response
|
||||||
|
expect(Notification.where(notification_type: 'sla_missed_next_response').count).to eq(2)
|
||||||
|
# Check if notification is created for the assignee
|
||||||
|
expect(Notification.where(user_id: user_1.id).count).to eq(1)
|
||||||
|
# Check if notification is created for the account admin
|
||||||
|
expect(Notification.where(user_id: admin.id).count).to eq(1)
|
||||||
|
# Check if no notification is created for other user
|
||||||
|
expect(Notification.where(user_id: user_2.id).count).to eq(0)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -47,6 +71,15 @@ RSpec.describe Sla::EvaluateAppliedSlaService do
|
|||||||
expect(Rails.logger).to have_received(:warn).with("SLA missed for conversation #{conversation.id} in account " \
|
expect(Rails.logger).to have_received(:warn).with("SLA missed for conversation #{conversation.id} in account " \
|
||||||
"#{applied_sla.account_id} for sla_policy #{sla_policy.id}")
|
"#{applied_sla.account_id} for sla_policy #{sla_policy.id}")
|
||||||
expect(applied_sla.reload.sla_status).to eq('missed')
|
expect(applied_sla.reload.sla_status).to eq('missed')
|
||||||
|
|
||||||
|
expect(Notification.count).to eq(2)
|
||||||
|
expect(Notification.where(notification_type: 'sla_missed_resolution').count).to eq(2)
|
||||||
|
# Check if notification is created for the assignee
|
||||||
|
expect(Notification.where(user_id: user_1.id).count).to eq(1)
|
||||||
|
# Check if notification is created for the account admin
|
||||||
|
expect(Notification.where(user_id: admin.id).count).to eq(1)
|
||||||
|
# Check if no notification is created for other user
|
||||||
|
expect(Notification.where(user_id: user_2.id).count).to eq(0)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -76,6 +109,7 @@ RSpec.describe Sla::EvaluateAppliedSlaService do
|
|||||||
expect(Rails.logger).to have_received(:warn).with("SLA missed for conversation #{conversation.id} in account " \
|
expect(Rails.logger).to have_received(:warn).with("SLA missed for conversation #{conversation.id} in account " \
|
||||||
"#{applied_sla.account_id} for sla_policy #{sla_policy.id}").exactly(1).time
|
"#{applied_sla.account_id} for sla_policy #{sla_policy.id}").exactly(1).time
|
||||||
expect(applied_sla.reload.sla_status).to eq('missed')
|
expect(applied_sla.reload.sla_status).to eq('missed')
|
||||||
|
expect(Notification.count).to eq(2)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -99,6 +133,7 @@ RSpec.describe Sla::EvaluateAppliedSlaService do
|
|||||||
expect(Rails.logger).to have_received(:info).with("SLA hit for conversation #{conversation.id} in account " \
|
expect(Rails.logger).to have_received(:info).with("SLA hit for conversation #{conversation.id} in account " \
|
||||||
"#{applied_sla.account_id} for sla_policy #{sla_policy.id}")
|
"#{applied_sla.account_id} for sla_policy #{sla_policy.id}")
|
||||||
expect(applied_sla.reload.sla_status).to eq('hit')
|
expect(applied_sla.reload.sla_status).to eq('hit')
|
||||||
|
expect(Notification.count).to eq(0)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user